test: add e2e tests (#21)
* test: add e2e tests Spin up GCP resources for testing dynamic host catalogs. The resources are tested with tests that focus on credential changes and rotation for host catalog and host set creation and updates using labels. - The tests all depend on each other so they need to run in order. - The test can also only be executed locally for now. - The resources are cleaned up after the test as part of the test.
This commit is contained in:
1 parent
baa2c7a2e7
commit
d3a155bd3f
11 files changed
+1274
-103
No files matched your search
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0
|
github.com/google/go-cmp v0.6.0
|
||||||
github.com/googleapis/gax-go/v2 v2.13.0
|
github.com/googleapis/gax-go/v2 v2.13.0
|
||||||
github.com/hashicorp/boundary/sdk v0.0.47
|
github.com/hashicorp/boundary/sdk v0.0.47
|
||||||
|
github.com/hashicorp/terraform-json v0.23.0
|
||||||
golang.org/x/oauth2 v0.22.0
|
golang.org/x/oauth2 v0.22.0
|
||||||
google.golang.org/api v0.196.0
|
google.golang.org/api v0.196.0
|
||||||
)
|
)
|
||||||
@@ -18,12 +19,15 @@ require (
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||||
cloud.google.com/go/longrunning v0.6.0 // indirect
|
cloud.google.com/go/longrunning v0.6.0 // indirect
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/s2a-go v0.1.8 // indirect
|
github.com/google/s2a-go v0.1.8 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
|
github.com/zclconf/go-cty v1.15.0 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y
|
|||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||||
@@ -125,8 +127,12 @@ github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3ly
|
|||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
|
||||||
|
github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
|
||||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||||
@@ -187,6 +193,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
|
||||||
|
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||||
|
|||||||
@@ -0,0 +1,589 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||||
|
"github.com/hashicorp/boundary-plugin-gcp/plugin/service/host"
|
||||||
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||||
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||||
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
expectedInstanceCount = 3
|
||||||
|
expectedTagsCount = 3
|
||||||
|
expectedLabelsCount = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// All the tests in this test depend on each other
|
||||||
|
// to run in order. This is because the tests are
|
||||||
|
// testing the same plugin instance and the same
|
||||||
|
// persisted state. The tests are designed to run
|
||||||
|
// in order to test the plugin's ability to rotate
|
||||||
|
// credentials and manage the state of the plugin
|
||||||
|
// instance.
|
||||||
|
// To run the tests you need the following
|
||||||
|
// environment variables set:
|
||||||
|
// GOOGLE_APPLICATION_CREDENTIALS: This should be
|
||||||
|
// the path to the service account key. This can also
|
||||||
|
// be automatically set after authenticating with gcloud
|
||||||
|
// using `gcloud auth application-default login`.
|
||||||
|
//
|
||||||
|
// TF_VAR_project_id: The project_id of the project.
|
||||||
|
//
|
||||||
|
// TF_VAR_region: The region the resources should be
|
||||||
|
// deployed in.
|
||||||
|
//
|
||||||
|
// TF_VAR_zone: The zone the resources should be
|
||||||
|
// deployed in.
|
||||||
|
func TestHostPlugin(t *testing.T) {
|
||||||
|
creds := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||||
|
if creds == "" {
|
||||||
|
t.Skip("set GOOGLE_APPLICATION_CREDENTIALS to use this test")
|
||||||
|
}
|
||||||
|
projectId := os.Getenv("TF_VAR_project_id")
|
||||||
|
if projectId == "" {
|
||||||
|
t.Skip("set TF_VAR_project_id to use this test")
|
||||||
|
}
|
||||||
|
region := os.Getenv("TF_VAR_region")
|
||||||
|
if region == "" {
|
||||||
|
t.Skip("set TF_VAR_region to use this test")
|
||||||
|
}
|
||||||
|
zone := os.Getenv("TF_VAR_zone")
|
||||||
|
if zone == "" {
|
||||||
|
t.Skip("set TF_VAR_zone to use this test")
|
||||||
|
}
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
tf, err := NewTestTerraformer("testdata/host")
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(tf)
|
||||||
|
|
||||||
|
t.Log("===== deploying test Terraform workspace =====")
|
||||||
|
err = tf.Deploy()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
t.Log("===== destroying test Terraform workspace =====")
|
||||||
|
if err := tf.Destroy(); err != nil {
|
||||||
|
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientEmail, err := tf.GetOutput("client_email")
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(clientEmail)
|
||||||
|
|
||||||
|
instanceIds, err := tf.GetOutputSlice("instance_ids")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(instanceIds, expectedInstanceCount)
|
||||||
|
|
||||||
|
instanceTags, err := tf.GetOutputMap("instance_tags")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(instanceTags, expectedInstanceCount)
|
||||||
|
|
||||||
|
instanceLabels, err := tf.GetOutputMap("instance_labels")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(instanceLabels, expectedLabelsCount)
|
||||||
|
|
||||||
|
privateKeys, err := tf.GetOutputSlice("private_keys")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(privateKeys, expectedServiceAccountKeys)
|
||||||
|
|
||||||
|
privateKeyIds, err := tf.GetOutputSlice("private_key_ids")
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(privateKeyIds, expectedServiceAccountKeys)
|
||||||
|
|
||||||
|
p := new(host.HostPlugin)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var keyId, secret string
|
||||||
|
|
||||||
|
t.Run("OnCreateCatalog", func(t *testing.T) {
|
||||||
|
t.Run("Test non-rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnCreateCatalog(
|
||||||
|
ctx,
|
||||||
|
t,
|
||||||
|
p,
|
||||||
|
zone,
|
||||||
|
projectId,
|
||||||
|
clientEmail.(string),
|
||||||
|
privateKeyIds[0].(string),
|
||||||
|
privateKeys[0].(string),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnCreateCatalog(
|
||||||
|
ctx,
|
||||||
|
t,
|
||||||
|
p,
|
||||||
|
zone,
|
||||||
|
projectId,
|
||||||
|
clientEmail.(string),
|
||||||
|
keyId,
|
||||||
|
secret,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OnUpdateCatalog", func(t *testing.T) {
|
||||||
|
t.Run("Test no-op non-rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, "", "", false, false)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Switch to rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, "", "", false, true)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test no-op with rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, "", "", true, true)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Switch credentials to next service account key. Don't rotate", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, privateKeyIds[1].(string), privateKeys[1].(string), true, false)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Switch credentials to next service account key. Add rotation", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, privateKeyIds[2].(string), privateKeys[2].(string), false, true)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Switch to next service account key, with rotation disabled", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, privateKeyIds[3].(string), privateKeys[3].(string), true, false)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Switch to another service account key and keep rotation off", func(t *testing.T) {
|
||||||
|
keyId, secret = testPluginOnUpdateCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, privateKeyIds[4].(string), privateKeys[4].(string), false, false)
|
||||||
|
require.NotNil(keyId)
|
||||||
|
require.NotNil(secret)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OnDeleteCatalog", func(t *testing.T) {
|
||||||
|
t.Run("Test non-rotated", func(t *testing.T) {
|
||||||
|
testPluginOnDeleteCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test as if we had rotated these credentials.", func(t *testing.T) {
|
||||||
|
testPluginOnDeleteCatalog(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HostSet", func(t *testing.T) {
|
||||||
|
keyId, secret := privateKeyIds[5].(string), privateKeys[5].(string)
|
||||||
|
expectedLabelInstancesMap := make(map[string][]string)
|
||||||
|
for instanceId, instanceLabel := range instanceLabels {
|
||||||
|
for tagKey := range instanceLabel.(map[string]any) {
|
||||||
|
for _, expectedTag := range instanceLabels {
|
||||||
|
if tagKey == expectedTag {
|
||||||
|
expectedLabelInstancesMap[tagKey] = append(expectedLabelInstancesMap[tagKey], instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := map[string][]string{
|
||||||
|
"label-1": {"foo"},
|
||||||
|
"label-2": {"bar"},
|
||||||
|
"label-3": {"baz"},
|
||||||
|
"label-4": {"foo", "bar"},
|
||||||
|
"label-5": {"foo", "baz"},
|
||||||
|
"label-6": {"bar", "baz"},
|
||||||
|
"label-7": {"foo", "bar", "baz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, tc := range cases {
|
||||||
|
t.Run(fmt.Sprintf("Label=%s", key), func(t *testing.T) {
|
||||||
|
labels := make([]string, 0, len(tc))
|
||||||
|
for _, label := range tc {
|
||||||
|
labels = append(labels, fmt.Sprintf("labels.%s=%s", key, label))
|
||||||
|
}
|
||||||
|
testPluginOnCreateUpdateSet(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, labels)
|
||||||
|
testPluginListHosts(ctx, t, p, zone, projectId, clientEmail.(string), keyId, secret, labels, expectedLabelInstancesMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPluginOnCreateCatalog(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
p *host.HostPlugin,
|
||||||
|
zone string,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string,
|
||||||
|
rotate bool) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing OnCreateCatalog (zone=%s, rotate=%t)", zone, rotate)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotate,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: privateKeyId,
|
||||||
|
credential.ConstPrivateKey: privateKey,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
request := &pb.OnCreateCatalogRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqAttrs,
|
||||||
|
},
|
||||||
|
Secrets: reqSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.OnCreateCatalog(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
persisted := response.GetPersisted()
|
||||||
|
require.NotNil(persisted)
|
||||||
|
return validatePersistedSecrets(t, persisted.GetSecrets(), projectId, clientEmail, privateKeyId, privateKey, rotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPluginOnUpdateCatalog(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
p *host.HostPlugin,
|
||||||
|
zone string,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
currentPrivateKeyId string,
|
||||||
|
currentPrivateKey string,
|
||||||
|
newPrivateKeyId string,
|
||||||
|
newPrivateKey string,
|
||||||
|
rotated bool,
|
||||||
|
rotate bool,
|
||||||
|
) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing OnUpdateCatalog (zone=%s, newcreds=%t, rotated=%t, rotate=%t)", zone, newPrivateKeyId != "" && newPrivateKey != "", rotated, rotate)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// Take a timestamp of the current time to get a point in time to
|
||||||
|
// reference, ensuring that we are updating credential rotation
|
||||||
|
// timestamps.
|
||||||
|
currentCredsLastRotatedTime := time.Now()
|
||||||
|
|
||||||
|
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotated,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotate,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
var reqSecrets *structpb.Struct
|
||||||
|
if newPrivateKeyId != "" && newPrivateKey != "" {
|
||||||
|
reqSecrets, err = structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: newPrivateKeyId,
|
||||||
|
credential.ConstPrivateKey: newPrivateKey,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
}
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: currentPrivateKeyId,
|
||||||
|
credential.ConstPrivateKey: currentPrivateKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: func() string {
|
||||||
|
if rotated {
|
||||||
|
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (time.Time{}).Format(time.RFC3339Nano)
|
||||||
|
}(),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(reqPersistedSecrets)
|
||||||
|
request := &pb.OnUpdateCatalogRequest{
|
||||||
|
CurrentCatalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqCurrentAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewCatalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqNewAttrs,
|
||||||
|
},
|
||||||
|
Secrets: reqSecrets,
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.OnUpdateCatalog(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
persisted := response.GetPersisted()
|
||||||
|
require.NotNil(persisted)
|
||||||
|
return validateUpdateSecrets(
|
||||||
|
t,
|
||||||
|
projectId,
|
||||||
|
clientEmail,
|
||||||
|
persisted.GetSecrets(),
|
||||||
|
currentCredsLastRotatedTime,
|
||||||
|
currentPrivateKeyId,
|
||||||
|
currentPrivateKey,
|
||||||
|
newPrivateKeyId,
|
||||||
|
newPrivateKey,
|
||||||
|
rotated,
|
||||||
|
rotate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPluginOnDeleteCatalog(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
p *host.HostPlugin,
|
||||||
|
zone string,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string,
|
||||||
|
rotated bool) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing OnDeleteCatalog (zone=%s, rotated=%t)", zone, rotated)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotated,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: privateKeyId,
|
||||||
|
credential.ConstPrivateKey: privateKey,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: privateKeyId,
|
||||||
|
credential.ConstPrivateKey: privateKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: func() string {
|
||||||
|
if rotated {
|
||||||
|
return time.Now().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (time.Time{}).Format(time.RFC3339Nano)
|
||||||
|
}(),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
request := &pb.OnDeleteCatalogRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqAttrs,
|
||||||
|
},
|
||||||
|
Secrets: reqSecrets,
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.OnDeleteCatalog(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
|
||||||
|
// We want to test the validity of the credentials post-deletion.
|
||||||
|
if rotated {
|
||||||
|
// The credentials should no longer be valid.
|
||||||
|
requireCredentialsInvalid(t, projectId, clientEmail, privateKeyId, privateKey)
|
||||||
|
} else {
|
||||||
|
// The credentials should still be valid. Sleep 10s first just to
|
||||||
|
// be sure, since we're not rotating.
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
requireCredentialsValid(t, projectId, clientEmail, privateKeyId, privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPluginOnCreateUpdateSet(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
p *host.HostPlugin,
|
||||||
|
zone string,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string,
|
||||||
|
labels []string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing OnCreateSet (zone=%s, labels=%v)", zone, labels)
|
||||||
|
require := require.New(t)
|
||||||
|
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
host.ConstListInstancesFilter: []any{strings.Join(labels, " AND ")},
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: privateKeyId,
|
||||||
|
credential.ConstPrivateKey: privateKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
createRequest := &pb.OnCreateSetRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Set: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createResponse, err := p.OnCreateSet(ctx, createRequest)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(createResponse)
|
||||||
|
|
||||||
|
// Do an update test in the same function, as it's pretty much the
|
||||||
|
// same function right now.
|
||||||
|
t.Logf("testing OnUpdateSet (zone=%s, labels=%v)", zone, labels)
|
||||||
|
updateRequest := &pb.OnUpdateSetRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CurrentSet: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewSet: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
updateResponse, err := p.OnUpdateSet(ctx, updateRequest)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(updateResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPluginListHosts(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
p *host.HostPlugin,
|
||||||
|
zone string,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string,
|
||||||
|
labels []string,
|
||||||
|
expected map[string][]string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing ListHosts (zone=%s, labels=%v)", zone, labels)
|
||||||
|
require := require.New(t)
|
||||||
|
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstProjectId: projectId,
|
||||||
|
credential.ConstClientEmail: clientEmail,
|
||||||
|
credential.ConstZone: zone,
|
||||||
|
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
sets := make([]*hostsets.HostSet, len(labels))
|
||||||
|
for i, label := range labels {
|
||||||
|
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
host.ConstListInstancesFilter: []any{label},
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
sets[i] = &hostsets.HostSet{
|
||||||
|
Id: fmt.Sprintf("hostset-%d", i),
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstPrivateKeyId: privateKeyId,
|
||||||
|
credential.ConstPrivateKey: privateKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
request := &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sets: sets,
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.ListHosts(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
|
||||||
|
// Validate the returned instances by ID. Assemble the instance
|
||||||
|
// details from the expected set.
|
||||||
|
expectedInstances := make(map[string][]string)
|
||||||
|
for i, label := range labels {
|
||||||
|
for _, instanceId := range expected[label] {
|
||||||
|
expectedInstances[instanceId] = append(expectedInstances[instanceId], fmt.Sprintf("hostset-%d", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the returned hosts by ID and create the same kind of map.
|
||||||
|
actualInstances := make(map[string][]string)
|
||||||
|
for _, host := range response.GetHosts() {
|
||||||
|
actualInstances[host.ExternalId] = host.SetIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare
|
||||||
|
require.Equal(expectedInstances, actualInstances)
|
||||||
|
// Success
|
||||||
|
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", zone, labels, len(actualInstances), actualInstances)
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (c) HashiCorp, Inc.
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
data "google_compute_zones" "available" {}
|
|
||||||
|
|
||||||
resource "google_compute_instance" "default" {
|
|
||||||
count = var.num_instances
|
|
||||||
name = "boundary-${count.index}"
|
|
||||||
machine_type = "n2-standard-2"
|
|
||||||
zone = data.google_compute_zones.available.names[0]
|
|
||||||
|
|
||||||
labels = var.labels
|
|
||||||
|
|
||||||
boot_disk {
|
|
||||||
initialize_params {
|
|
||||||
image = "ubuntu-os-cloud/ubuntu-2204-lts"
|
|
||||||
labels = var.labels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
network_interface {
|
|
||||||
network = "default"
|
|
||||||
|
|
||||||
access_config {
|
|
||||||
// Ephemeral public IP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata = {
|
|
||||||
ssh-keys = "ubuntu:${tls_private_key.ssh.public_key_openssh}"
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamic "service_account" {
|
|
||||||
for_each = var.service_account_email != null ? [var.service_account_email] : []
|
|
||||||
content {
|
|
||||||
email = service_account.value
|
|
||||||
scopes = ["cloud-platform"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "google_compute_instance_group" "webservers" {
|
|
||||||
name = "boundary-servers"
|
|
||||||
description = "Boundary server group"
|
|
||||||
|
|
||||||
instances = google_compute_instance.default.*.id
|
|
||||||
|
|
||||||
zone = data.google_compute_zones.available.names[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
# RSA key of size 4096 bits
|
|
||||||
resource "tls_private_key" "ssh" {
|
|
||||||
algorithm = "RSA"
|
|
||||||
rsa_bits = 4096
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "google_compute_firewall" "rules" {
|
|
||||||
name = "allow-ssh"
|
|
||||||
network = "default"
|
|
||||||
|
|
||||||
allow {
|
|
||||||
protocol = "tcp"
|
|
||||||
ports = ["22"]
|
|
||||||
}
|
|
||||||
source_ranges = [var.client_cidr_block]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) HashiCorp, Inc.
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
resource "local_file" "ssh_key" {
|
|
||||||
content = tls_private_key.ssh.private_key_openssh
|
|
||||||
filename = "${path.module}/secrets/id_rsa.pem"
|
|
||||||
file_permission = "0400"
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Copyright (c) HashiCorp, Inc.
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
variable "labels" {
|
|
||||||
type = map(string)
|
|
||||||
default = {
|
|
||||||
repository = "boundary-plugin-gcp"
|
|
||||||
purpose = "testing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "region" {
|
|
||||||
type = string
|
|
||||||
default = "us-central1"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "num_instances" {
|
|
||||||
type = number
|
|
||||||
default = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "service_account_email" {
|
|
||||||
type = string
|
|
||||||
default = null
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "client_cidr_block" {
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
Vendored
+77
@@ -0,0 +1,77 @@
|
|||||||
|
# Copyright (c) HashiCorp, Inc.
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
resource "random_id" "service_account_client_email" {
|
||||||
|
prefix = "test-${var.user_prefix}"
|
||||||
|
byte_length = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "test_service_account" {
|
||||||
|
project = var.project_id
|
||||||
|
account_id = random_id.service_account_client_email.dec
|
||||||
|
display_name = "test-${var.user_prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_project_iam_binding" "test_service_account_user" {
|
||||||
|
project = var.project_id
|
||||||
|
count = length(var.rolesList)
|
||||||
|
role = var.rolesList[count.index]
|
||||||
|
members = [
|
||||||
|
"serviceAccount:${google_service_account.test_service_account.email}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account_key" "test_service_account_key" {
|
||||||
|
service_account_id = google_service_account.test_service_account.name
|
||||||
|
public_key_type = "TYPE_X509_PEM_FILE"
|
||||||
|
private_key_type = "TYPE_GOOGLE_CREDENTIALS_FILE"
|
||||||
|
key_algorithm = "KEY_ALG_RSA_2048"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account_key" "test_service_account_keys" {
|
||||||
|
count = var.num_service_account_keys
|
||||||
|
service_account_id = google_service_account.test_service_account.name
|
||||||
|
public_key_type = "TYPE_X509_PEM_FILE"
|
||||||
|
private_key_type = "TYPE_GOOGLE_CREDENTIALS_FILE"
|
||||||
|
key_algorithm = "KEY_ALG_RSA_2048"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_instance" "instances" {
|
||||||
|
project = var.project_id
|
||||||
|
count = var.num_instances
|
||||||
|
name = "boundary-${count.index}"
|
||||||
|
machine_type = "n2-standard-2"
|
||||||
|
zone = var.zone
|
||||||
|
labels = var.labels
|
||||||
|
tags = ["test-instance-${count.index}"]
|
||||||
|
|
||||||
|
boot_disk {
|
||||||
|
initialize_params {
|
||||||
|
image = "ubuntu-os-cloud/ubuntu-2204-lts"
|
||||||
|
labels = var.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
network = "default"
|
||||||
|
|
||||||
|
access_config {
|
||||||
|
// Ephemeral public IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
ssh-keys = "ubuntu:${tls_private_key.ssh.public_key_openssh}"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_account {
|
||||||
|
email = google_service_account.test_service_account.email
|
||||||
|
scopes = ["cloud-platform"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# RSA key of size 4096 bits
|
||||||
|
resource "tls_private_key" "ssh" {
|
||||||
|
algorithm = "RSA"
|
||||||
|
rsa_bits = 4096
|
||||||
|
}
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright (c) HashiCorp, Inc.
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
output "client_email" {
|
||||||
|
value = google_service_account.test_service_account.email
|
||||||
|
}
|
||||||
|
|
||||||
|
output "private_keys" {
|
||||||
|
value = [for key in google_service_account_key.test_service_account_keys : jsondecode(base64decode(key.private_key)).private_key]
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "private_key_ids" {
|
||||||
|
value = [for key in google_service_account_key.test_service_account_keys : chomp(reverse(split("/", key.id))[0])]
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "instance_ids" {
|
||||||
|
value = google_compute_instance.instances.*.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "instance_tags" {
|
||||||
|
value = {
|
||||||
|
for instance in google_compute_instance.instances : instance.id => instance.tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "instance_labels" {
|
||||||
|
value = {
|
||||||
|
for instance in google_compute_instance.instances : instance.id => instance.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@ terraform {
|
|||||||
|
|
||||||
provider "google" {
|
provider "google" {
|
||||||
region = var.region
|
region = var.region
|
||||||
|
project = var.project_id
|
||||||
}
|
}
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Copyright (c) HashiCorp, Inc.
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
data "google_client_openid_userinfo" "current" {}
|
||||||
|
|
||||||
|
variable "labels" {
|
||||||
|
type = map(string)
|
||||||
|
default = {
|
||||||
|
repository = "boundary-plugin-gcp"
|
||||||
|
purpose = "testing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "project_id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "zone" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "num_instances" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "service_account_email" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rolesList" {
|
||||||
|
type = list(string)
|
||||||
|
description = "List of roles to assign to the service account"
|
||||||
|
default = ["roles/compute.viewer","roles/iam.serviceAccountKeyAdmin"]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_prefix" {
|
||||||
|
type = string
|
||||||
|
default = "boundary-gcp-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "num_service_account_keys" {
|
||||||
|
type = number
|
||||||
|
default = 6
|
||||||
|
}
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||||
|
"github.com/hashicorp/boundary-plugin-gcp/internal/values"
|
||||||
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// expectedServiceAccountKeys is the number of service account keys
|
||||||
|
// expected to be created in the Terraform project.
|
||||||
|
expectedServiceAccountKeys = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTerraformer is an object representing the lifecycle of a
|
||||||
|
// particular Terraform project.
|
||||||
|
//
|
||||||
|
// Note that Terraform must be installed in the system PATH (not to
|
||||||
|
// be confused with the working directory below). This object does
|
||||||
|
// not manage anything with regards to installation or version
|
||||||
|
// management.
|
||||||
|
type TestTerraformer struct {
|
||||||
|
// The full path to the Terraform binary, set during initialization
|
||||||
|
// to the binary found in the system path.
|
||||||
|
tfPath string
|
||||||
|
|
||||||
|
// The working directory for Terraform commands.
|
||||||
|
workDir string
|
||||||
|
|
||||||
|
// The Terraform state. Use the Output method to look up particular
|
||||||
|
// outputs from the Terraform state.
|
||||||
|
state *tfjson.State
|
||||||
|
|
||||||
|
// Keeps track of whether or not we have run the init command
|
||||||
|
// during this run of Terraform. This only needs to be done once.
|
||||||
|
// Do not use initErr directly; it stores the init error if one was
|
||||||
|
// encountered on init.
|
||||||
|
initOnce sync.Once
|
||||||
|
initErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestTerraformer initializes a Terraform directory. It just
|
||||||
|
// returns an initialized project, it does not perform any actions on
|
||||||
|
// it.
|
||||||
|
func NewTestTerraformer(workDir string) (*TestTerraformer, error) {
|
||||||
|
tfPath, err := exec.LookPath("terraform")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LookPath error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir == "" {
|
||||||
|
return nil, errors.New("workDir is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TestTerraformer{
|
||||||
|
tfPath: tfPath,
|
||||||
|
workDir: workDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy runs the following commands, in order: init, apply, plan,
|
||||||
|
// json. The output of the JSON command is then imported to the
|
||||||
|
// internal state.
|
||||||
|
func (t *TestTerraformer) Deploy() error {
|
||||||
|
if err := t.init(); err != nil {
|
||||||
|
return fmt.Errorf("error running Terraform init: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.apply(); err != nil {
|
||||||
|
return fmt.Errorf("error running Terraform apply: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.plan(); err != nil {
|
||||||
|
return fmt.Errorf("error running Terraform plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := t.showJson()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting Terraform plan JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := new(tfjson.Plan)
|
||||||
|
if err := json.Unmarshal(output, newState); err != nil {
|
||||||
|
return fmt.Errorf("error reading new Terraform plan JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.state = newState.PriorState
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy runs the following commands, in order: init, destroy. The
|
||||||
|
// state is also cleared.
|
||||||
|
func (t *TestTerraformer) Destroy() error {
|
||||||
|
if err := t.init(); err != nil {
|
||||||
|
return fmt.Errorf("error running Terraform init: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.destroy(); err != nil {
|
||||||
|
return fmt.Errorf("error running Terraform destroy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.state = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutput returns the value of the output located at key.
|
||||||
|
//
|
||||||
|
// Outputs for the root module are supported only. It's an error to
|
||||||
|
// run this when no state exists.
|
||||||
|
func (t *TestTerraformer) GetOutput(key string) (any, error) {
|
||||||
|
if key == "" {
|
||||||
|
return nil, errors.New("key must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.state == nil {
|
||||||
|
return nil, errors.New("state is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.state.Values == nil {
|
||||||
|
return nil, errors.New("state integrity error: no state values")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.state.Values.Outputs == nil {
|
||||||
|
return nil, errors.New("state integrity error: outputs is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, ok := t.state.Values.Outputs[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("output with key %q not found", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("state integrity error: output at key %q is nil", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutputString wraps GetOutput and returns the value as a string. It's an
|
||||||
|
// error if the return value is actually not a string.
|
||||||
|
func (t *TestTerraformer) GetOutputString(key string) (string, error) {
|
||||||
|
value, err := t.GetOutput(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
str, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("expected output value at key %q to be string, got %T", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutputMap wraps GetOutput and returns the value as a map. It's an
|
||||||
|
// error if the return value is actually not a map.
|
||||||
|
func (t *TestTerraformer) GetOutputMap(key string) (map[string]any, error) {
|
||||||
|
value, err := t.GetOutput(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, ok := value.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected output value at key %q to be map, got %T", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutputSlice wraps GetOutput and returns the value as a slice. It's an
|
||||||
|
// error if the return value is actually not a slice.
|
||||||
|
func (t *TestTerraformer) GetOutputSlice(key string) ([]any, error) {
|
||||||
|
value, err := t.GetOutput(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected output value at key %q to be slice, got %T", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init runs "terraform init" in the project's working directory.
|
||||||
|
//
|
||||||
|
// This is only ever done once.
|
||||||
|
func (t *TestTerraformer) init() error {
|
||||||
|
t.initOnce.Do(func() {
|
||||||
|
cmd := exec.Command(t.tfPath, "init")
|
||||||
|
cmd.Dir = t.workDir
|
||||||
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.initErr = fmt.Errorf("%s\n%s", stdoutStderr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return t.initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply runs "terraform apply" in the project's working directory.
|
||||||
|
func (t *TestTerraformer) apply() error {
|
||||||
|
cmd := exec.Command(t.tfPath, "apply", "-input=false", "-auto-approve")
|
||||||
|
cmd.Dir = t.workDir
|
||||||
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy runs "terraform destroy" in the project's working directory.
|
||||||
|
func (t *TestTerraformer) destroy() error {
|
||||||
|
cmd := exec.Command(t.tfPath, "destroy", "-input=false", "-auto-approve")
|
||||||
|
cmd.Dir = t.workDir
|
||||||
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// plan runs "terraform plan" in the project's working directory.
|
||||||
|
//
|
||||||
|
// Note that plan is not run as part of the apply workflow; rather,
|
||||||
|
// it's run to get the data for for importing the state via
|
||||||
|
// terraform-json. The apply method ignores the plan generated here.
|
||||||
|
func (t *TestTerraformer) plan() error {
|
||||||
|
cmd := exec.Command(t.tfPath, "plan", "-input=false", "-out=plan.tfplan")
|
||||||
|
cmd.Dir = t.workDir
|
||||||
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// showJson runs "terraform show -json". The output is returned along
|
||||||
|
// with any error.
|
||||||
|
//
|
||||||
|
// Output is only the results of stdout and should be suitable for
|
||||||
|
// parsing.
|
||||||
|
//
|
||||||
|
// This method expects the plan.tfplan file to be generated in the
|
||||||
|
// working directory already. This can be done with the plan method.
|
||||||
|
func (t *TestTerraformer) showJson() ([]byte, error) {
|
||||||
|
cmd := exec.Command(t.tfPath, "show", "-json", "plan.tfplan")
|
||||||
|
cmd.Dir = t.workDir
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
exitErr, ok := err.(*exec.ExitError)
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("%s\n%s", exitErr.Stderr, exitErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireCredentialsInvalid(
|
||||||
|
t *testing.T,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
c, err := credential.NewConfig(
|
||||||
|
credential.WithProjectId(projectId),
|
||||||
|
credential.WithClientEmail(clientEmail),
|
||||||
|
credential.WithPrivateKeyId(privateKeyId),
|
||||||
|
credential.WithPrivateKey(privateKey))
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
creds, err := c.GenerateCredentials(context.Background())
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// We need to wait for invalidation as while gcp waits for
|
||||||
|
// credential creation, deletion of the old credentials returns
|
||||||
|
// immediately.
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
waitErr:
|
||||||
|
for {
|
||||||
|
_, err = c.ValidateIamPermissions(timeoutCtx, []string{credential.ComputeInstancesListPermission}, option.WithTokenSource(creds.TokenSource))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
// pass
|
||||||
|
|
||||||
|
case <-timeoutCtx.Done():
|
||||||
|
break waitErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Error(err)
|
||||||
|
require.Contains(err.Error(), "failed to validate IAM permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireCredentialsValid(
|
||||||
|
t *testing.T,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
c, err := credential.NewConfig(
|
||||||
|
credential.WithProjectId(projectId),
|
||||||
|
credential.WithClientEmail(clientEmail),
|
||||||
|
credential.WithPrivateKeyId(privateKeyId),
|
||||||
|
credential.WithPrivateKey(privateKey))
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
creds, err := c.GenerateCredentials(context.Background())
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
_, err = c.ValidateIamPermissions(context.Background(), []string{credential.ComputeInstancesListPermission}, option.WithTokenSource(creds.TokenSource))
|
||||||
|
require.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePersistedSecrets(
|
||||||
|
t *testing.T,
|
||||||
|
persistedSecrets *structpb.Struct,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
privateKeyId string,
|
||||||
|
privateKey string,
|
||||||
|
rotate bool) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
require.NotNil(persistedSecrets)
|
||||||
|
persistedPrivateKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstPrivateKeyId, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedPrivateKeyId)
|
||||||
|
if rotate {
|
||||||
|
require.NotEqual(privateKeyId, persistedPrivateKeyId)
|
||||||
|
} else {
|
||||||
|
require.Equal(privateKeyId, persistedPrivateKeyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedPrivateKey, err := values.GetStringValue(persistedSecrets, credential.ConstPrivateKey, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedPrivateKey)
|
||||||
|
if rotate {
|
||||||
|
require.NotEqual(privateKey, persistedPrivateKey)
|
||||||
|
} else {
|
||||||
|
require.Equal(privateKey, persistedPrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||||
|
require.NoError(err)
|
||||||
|
if rotate {
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
requireCredentialsInvalid(t, projectId, clientEmail, privateKeyId, privateKey)
|
||||||
|
} else {
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedPrivateKeyId, persistedPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUpdateSecrets(
|
||||||
|
t *testing.T,
|
||||||
|
projectId string,
|
||||||
|
clientEmail string,
|
||||||
|
persistedSecrets *structpb.Struct,
|
||||||
|
currentCredsLastRotatedTime time.Time,
|
||||||
|
currentPrivateKeyId string,
|
||||||
|
currentPrivateKey string,
|
||||||
|
newPrivateKeyId string,
|
||||||
|
newPrivateKey string,
|
||||||
|
rotated bool,
|
||||||
|
rotate bool) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// Complex checks based on the scenarios.
|
||||||
|
persistedPrivateKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstPrivateKeyId, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedPrivateKeyId)
|
||||||
|
persistedPrivateKey, err := values.GetStringValue(persistedSecrets, credential.ConstPrivateKey, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedPrivateKey)
|
||||||
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Our test scenarios are complex due the multi-dimensional nature
|
||||||
|
// of criteria, so we lay them out in a switch below.
|
||||||
|
switch {
|
||||||
|
case newPrivateKeyId != "" && rotated && rotate:
|
||||||
|
// The new private key ID was provided, we had previously rotated
|
||||||
|
// the credentials before, and the new credential set is to be
|
||||||
|
// rotated as well. In this case, the old credentials should have
|
||||||
|
// been deleted, and the new credentials should have been rotated,
|
||||||
|
// hence, should not match the new credentials initially
|
||||||
|
// provided. Rotation time should be non-zero and updated.
|
||||||
|
requireCredentialsInvalid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.NotEqual(persistedPrivateKeyId, newPrivateKeyId)
|
||||||
|
require.NotEqual(persistedPrivateKey, newPrivateKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newPrivateKeyId != "" && rotated && !rotate:
|
||||||
|
// The new private key ID was provided, we had previously rotated
|
||||||
|
// the credentials before, and the new credential is *not*
|
||||||
|
// rotated. In this case, the old credentials should have
|
||||||
|
// been deleted, But the new credentials should have not been
|
||||||
|
// rotated, and hence should be the same. Rotation time should be
|
||||||
|
// zero.
|
||||||
|
requireCredentialsInvalid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.Equal(persistedPrivateKeyId, newPrivateKeyId)
|
||||||
|
require.Equal(persistedPrivateKey, newPrivateKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newPrivateKeyId != "" && !rotated && rotate:
|
||||||
|
// The new private key was provided, we *have not* previously
|
||||||
|
// rotated the credentials, and the new credential set is to be
|
||||||
|
// rotated. In this case, the old credentials should have been
|
||||||
|
// left alone, and the new credentials should have been rotated,
|
||||||
|
// hence, should not match the new credentials initially
|
||||||
|
// provided. Rotation time should be non-zero, but updated.
|
||||||
|
requireCredentialsValid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.NotEqual(persistedPrivateKeyId, newPrivateKeyId)
|
||||||
|
require.NotEqual(persistedPrivateKey, newPrivateKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newPrivateKeyId != "" && !rotated && !rotate:
|
||||||
|
// The new private key was provided, but we have not rotated
|
||||||
|
// the credentials previously and we still don't plan on rotating
|
||||||
|
// them. In this case, the old credentials should still be valid,
|
||||||
|
// and the persisted ones should match the new ones provided.
|
||||||
|
// Rotation time should be zero.
|
||||||
|
requireCredentialsValid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.Equal(persistedPrivateKeyId, newPrivateKeyId)
|
||||||
|
require.Equal(persistedPrivateKey, newPrivateKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newPrivateKeyId == "" && rotated && rotate:
|
||||||
|
// No new credentials have been provided, but we have previously
|
||||||
|
// rotated and are still rotating credentials. This is a no-op.
|
||||||
|
// Existing credentials should still be valid and match the ones
|
||||||
|
// persisted to state. Rotation time should be identical since
|
||||||
|
// no new rotation occurred.
|
||||||
|
requireCredentialsValid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.Equal(persistedPrivateKeyId, currentPrivateKeyId)
|
||||||
|
require.Equal(persistedPrivateKey, currentPrivateKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newPrivateKeyId == "" && rotated && !rotate:
|
||||||
|
// No new credentials have been provided, and we have previously
|
||||||
|
// rotated the credentials. This is actually an error, but we
|
||||||
|
// don't test it here; it's covered in unit testing (see
|
||||||
|
// plugin_test.go).
|
||||||
|
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||||
|
|
||||||
|
case newPrivateKeyId == "" && !rotated && rotate:
|
||||||
|
// No new credentials have been provided, and while we did not
|
||||||
|
// rotate before, we want to switch to rotation. In this case,
|
||||||
|
// the existing persisted credentials should have been rotated,
|
||||||
|
// with a new non-zero timestamp.
|
||||||
|
requireCredentialsInvalid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.NotEqual(persistedPrivateKeyId, currentPrivateKeyId)
|
||||||
|
require.NotEqual(persistedPrivateKey, currentPrivateKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newPrivateKeyId == "" && !rotated && !rotate:
|
||||||
|
// No new credentials have been provided and we have not, nor do
|
||||||
|
// not, plan on rotating the credentials. This is a no-op.
|
||||||
|
// Existing credentials should still be valid and match the ones
|
||||||
|
// persisted to state. Rotation time should remain at zero.
|
||||||
|
requireCredentialsValid(t, projectId, clientEmail, currentPrivateKeyId, currentPrivateKey)
|
||||||
|
require.Equal(persistedPrivateKeyId, currentPrivateKeyId)
|
||||||
|
require.Equal(persistedPrivateKey, currentPrivateKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Scenario was reached that was not covered by this function.
|
||||||
|
require.FailNow("unknown test scenario")
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedPrivateKeyId, persistedPrivateKey
|
||||||
|
}
|
||||||
Reference in new issue
Block a user