blob: 4e98ec2c2aa25b67c87f2ea76e767de8062f2474 [file] [edit]
/*
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
wipb "github.com/GoogleCloudPlatform/google-guest-agent/cmd/mwlid_extension/proto/mwlid"
)
const (
workloadRespTpl = `
{
"status": "OK",
"workloadCredentials": {
"%s": {
"certificatePem": "%s",
"privateKeyPem": "%s"
}
}
}
`
trustAnchorRespTpl = `
{
"status": "Ok",
"trustAnchors": {
"%s": {
"trustAnchorsPem": "%s"
},
"%s": {
"trustAnchorsPem": "%s"
}
}
}
`
testConfigStatusResp = `
{
"status": "Ok"
}
`
)
func TestWorkloadIdentitiesUnmarshal(t *testing.T) {
certPem := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
pvtPem := "-----BEGIN PRIVATE KEY-----datahere-----END PRIVATE KEY-----"
spiffe := "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
resp := fmt.Sprintf(workloadRespTpl, spiffe, certPem, pvtPem)
want := workloadIdentities{
Status: "OK",
WorkloadCredentials: map[string]workloadCredential{
spiffe: {
CertificatePem: certPem,
PrivateKeyPem: pvtPem,
},
},
}
got := workloadIdentities{}
if err := json.Unmarshal([]byte(resp), &got); err != nil {
t.Errorf("WorkloadIdentities.UnmarshalJSON(%s) failed unexpectedly with error: %v", resp, err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Workload identities unexpected diff (-want +got):\n%s", diff)
}
}
func TestTrustAnchorsUnmarshal(t *testing.T) {
domain1 := "12345.global.67890.workload.id.goog"
pem1 := "-----BEGIN CERTIFICATE-----datahere1-----END CERTIFICATE-----"
domain2 := "PEER_SPIFFE_TRUST_DOMAIN_2"
pem2 := "-----BEGIN CERTIFICATE-----datahere2-----END CERTIFICATE-----"
resp := fmt.Sprintf(trustAnchorRespTpl, domain1, pem1, domain2, pem2)
want := workloadTrustedAnchors{
Status: "Ok",
TrustAnchors: map[string]trustAnchor{
domain1: {
TrustAnchorsPem: pem1,
},
domain2: {
TrustAnchorsPem: pem2,
},
},
}
got := workloadTrustedAnchors{}
if err := json.Unmarshal([]byte(resp), &got); err != nil {
t.Errorf("WorkloadTrustedRootCerts.UnmarshalJSON(%s) failed unexpectedly with error: %v", resp, err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Workload trusted anchors diff (-want +got):\n%s", diff)
}
}
func TestWriteTrustAnchors(t *testing.T) {
spiffe := "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
domain1 := "12345.global.67890.workload.id.goog"
pem1 := "-----BEGIN CERTIFICATE-----datahere1-----END CERTIFICATE-----"
domain2 := "PEER_SPIFFE_TRUST_DOMAIN_2"
pem2 := "-----BEGIN CERTIFICATE-----datahere2-----END CERTIFICATE-----"
resp := fmt.Sprintf(trustAnchorRespTpl, domain1, pem1, domain2, pem2)
dir := t.TempDir()
if err := writeTrustAnchors([]byte(resp), dir, spiffe); err != nil {
t.Errorf("writeTrustAnchors(%s,%s,%s) failed unexpectedly with error %v", resp, dir, spiffe, err)
}
got, err := os.ReadFile(filepath.Join(dir, "ca_certificates.pem"))
if err != nil {
t.Errorf("failed to read file at %s with error: %v", filepath.Join(dir, "ca_certificates.pem"), err)
}
if string(got) != pem1 {
t.Errorf("writeTrustAnchors(%s,%s,%s) wrote %q, expected to write %q", resp, dir, spiffe, string(got), pem1)
}
}
func TestWriteWorkloadIdentities(t *testing.T) {
certPem := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
pvtPem := "-----BEGIN PRIVATE KEY-----datahere-----END PRIVATE KEY-----"
spiffe := "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
resp := fmt.Sprintf(workloadRespTpl, spiffe, certPem, pvtPem)
dir := t.TempDir()
gotID, err := writeWorkloadIdentities(dir, []byte(resp))
if err != nil {
t.Errorf("writeWorkloadIdentities(%s,%s) failed unexpectedly with error %v", dir, resp, err)
}
if gotID != spiffe {
t.Errorf("writeWorkloadIdentities(%s,%s) = %s, want %s", dir, resp, gotID, spiffe)
}
gotCertPem, err := os.ReadFile(filepath.Join(dir, "certificates.pem"))
if err != nil {
t.Errorf("failed to read file at %s with error: %v", filepath.Join(dir, "certificates.pem"), err)
}
if string(gotCertPem) != certPem {
t.Errorf("writeWorkloadIdentities(%s,%s) wrote %q, expected to write %q", dir, resp, string(gotCertPem), certPem)
}
gotPvtPem, err := os.ReadFile(filepath.Join(dir, "private_key.pem"))
if err != nil {
t.Errorf("failed to read file at %s with error: %v", filepath.Join(dir, "private_key.pem"), err)
}
if string(gotPvtPem) != pvtPem {
t.Errorf("writeWorkloadIdentities(%s,%s) wrote %q, expected to write %q", dir, resp, string(gotPvtPem), pvtPem)
}
}
func TestFindDomainError(t *testing.T) {
anchors := map[string]trustAnchor{
"67890.global.12345.workload.id.goog": {},
"55555.global.67890.workload.id.goog": {},
}
spiffeID := "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
if _, err := findDomain(anchors, spiffeID); err == nil {
t.Errorf("findDomain(%+v, %s) succeeded for unknown anchors, want error", anchors, spiffeID)
}
}
func TestFindDomain(t *testing.T) {
tests := []struct {
desc string
anchors map[string]trustAnchor
spiffeID string
want string
}{
{
desc: "single_trust_anchor",
anchors: map[string]trustAnchor{"12345.global.67890.workload.id.goog": {}},
spiffeID: "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID",
want: "12345.global.67890.workload.id.goog",
},
{
desc: "multiple_trust_anchor",
anchors: map[string]trustAnchor{
"67890.global.12345.workload.id.goog": {},
"12345.global.67890.workload.id.goog": {},
},
spiffeID: "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID",
want: "12345.global.67890.workload.id.goog",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
got, err := findDomain(test.anchors, test.spiffeID)
if err != nil {
t.Errorf("findDomain(%+v, %s) failed unexpectedly with error: %v", test.anchors, test.spiffeID, err)
}
if got != test.want {
t.Errorf("findDomain(%+v, %s) = %s, want %s", test.anchors, test.spiffeID, got, test.want)
}
})
}
}
// mdsTestClient is fake client to stub MDS response in unit tests.
type mdsTestClient struct {
// Is credential generation enabled.
enabled string
// Workload template.
spiffe, certPem, pvtPem string
// Trust Anchor template.
domain1, pem1, domain2, pem2 string
// Throw error on MDS request for "key".
throwErrOn string
}
func (mds *mdsTestClient) Get(ctx context.Context) (*metadata.Descriptor, error) {
return nil, fmt.Errorf("Get() not yet implemented")
}
func (mds *mdsTestClient) GetKey(ctx context.Context, key string, headers map[string]string) (string, error) {
if mds.throwErrOn == key {
return "", fmt.Errorf("this is fake error for testing")
}
switch key {
case enableWorkloadCertsKey:
return mds.enabled, nil
case configStatusKey:
return testConfigStatusResp, nil
case workloadIdentitiesKey:
return fmt.Sprintf(workloadRespTpl, mds.spiffe, mds.certPem, mds.pvtPem), nil
case trustAnchorsKey:
return fmt.Sprintf(trustAnchorRespTpl, mds.domain1, mds.pem1, mds.domain2, mds.pem2), nil
default:
return "", fmt.Errorf("unknown key %q", key)
}
}
func (mds *mdsTestClient) GetKeyRecursive(ctx context.Context, key string) (string, error) {
return "", fmt.Errorf("GetKeyRecursive() not yet implemented")
}
func (mds *mdsTestClient) Watch(ctx context.Context) (*metadata.Descriptor, error) {
return nil, fmt.Errorf("Watch() not yet implemented")
}
func (mds *mdsTestClient) WriteGuestAttributes(ctx context.Context, key string, value string) error {
return fmt.Errorf("WriteGuestattributes() not yet implemented")
}
func TestRefreshCreds(t *testing.T) {
ctx := context.Background()
tmp := t.TempDir()
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
// Templates to use in iterations.
spiffeTpl := "spiffe://12345.global.67890.workload.id.goog.%d/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
domain1Tpl := "12345.global.67890.workload.id.goog.%d"
pem1Tpl := "-----BEGIN CERTIFICATE-----datahere1.%d-----END CERTIFICATE-----"
domain2 := "PEER_SPIFFE_TRUST_DOMAIN_2_IGNORE"
pem2Tpl := "-----BEGIN CERTIFICATE-----datahere2.%d-----END CERTIFICATE-----"
certPemTpl := "-----BEGIN CERTIFICATE-----datahere.%d-----END CERTIFICATE-----"
pvtPemTpl := "-----BEGIN PRIVATE KEY-----datahere.%d-----END PRIVATE KEY-----"
contentPrefix := filepath.Join(tmp, "workload-spiffe-contents")
tmpSymlinkPrefix := filepath.Join(tmp, "workload-spiffe-symlink")
link := filepath.Join(tmp, "workload-spiffe-credentials")
out := outputOpts{contentPrefix, tmpSymlinkPrefix, link}
// Run refresh creds thrice to test updates.
// Link (workload-spiffe-credentials) should always refer to the updated
// content and previous directories should be removed.
for i := 1; i <= 3; i++ {
spiffe := fmt.Sprintf(spiffeTpl, i)
domain1 := fmt.Sprintf(domain1Tpl, i)
pem1 := fmt.Sprintf(pem1Tpl, i)
pem2 := fmt.Sprintf(pem2Tpl, i)
certPem := fmt.Sprintf(certPemTpl, i)
pvtPem := fmt.Sprintf(pvtPemTpl, i)
mdsClient := &mdsTestClient{
spiffe: spiffe,
certPem: certPem,
pvtPem: pvtPem,
domain1: domain1,
pem1: pem1,
domain2: domain2,
pem2: pem2,
enabled: "true",
}
j := &RefresherJob{mdsClient: mdsClient}
if err := j.refreshCreds(ctx, out, fmt.Sprintf("%d", i)); err != nil {
t.Fatalf("refreshCreds(ctx, %+v) failed unexpectedly with error: %v", out, err)
}
// Verify all files are created with the content as expected.
tests := []struct {
path string
content string
}{
{
path: filepath.Join(link, "ca_certificates.pem"),
content: pem1,
},
{
path: filepath.Join(link, "certificates.pem"),
content: certPem,
},
{
path: filepath.Join(link, "private_key.pem"),
content: pvtPem,
},
{
path: filepath.Join(link, "config_status"),
content: testConfigStatusResp,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
got, err := os.ReadFile(test.path)
if err != nil {
t.Errorf("failed to read expected file %q and content %q with error: %v", test.path, test.content, err)
}
if string(got) != test.content {
t.Errorf("refreshCreds(ctx, %+v) wrote %q, want content %q", out, string(got), test.content)
}
})
}
// Verify the symlink was created and references the right destination
// directory.
want := fmt.Sprintf("%s-%d", contentPrefix, i)
got, err := os.Readlink(link)
if err != nil {
t.Errorf("os.Readlink(%s) failed unexpectedly with error %v", link, err)
}
if got != want {
t.Errorf("os.Readlink(%s) = %s, want %s", link, got, want)
}
// If its not first run make sure prev creds are deleted.
if i > 1 {
prevDir := fmt.Sprintf("%s-%d", contentPrefix, i-1)
if _, err := os.Stat(prevDir); err == nil {
t.Errorf("os.Stat(%s) succeeded on prev content directory, want error", prevDir)
}
}
}
}
// TestRefreshCredsNoPrivatePem tests that the private_key.pem file is not
// written if the content from MDS is empty.
func TestRefreshCredsNoPrivatePem(t *testing.T) {
ctx := context.Background()
tmp := t.TempDir()
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
spiffe := "spiffe://12345.global.67890.workload.id.goog.0/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
domain1 := "12345.global.67890.workload.id.goog.0"
pem1 := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
domain2 := "PEER_SPIFFE_TRUST_DOMAIN_2_IGNORE"
pem2 := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
certPem := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
contentPrefix := filepath.Join(tmp, "workload-spiffe-contents")
tmpSymlinkPrefix := filepath.Join(tmp, "workload-spiffe-symlink")
link := filepath.Join(tmp, "workload-spiffe-credentials")
out := outputOpts{contentPrefix, tmpSymlinkPrefix, link}
mdsClient := &mdsTestClient{
spiffe: spiffe,
certPem: certPem,
domain1: domain1,
pem1: pem1,
domain2: domain2,
pem2: pem2,
enabled: "true",
}
j := &RefresherJob{mdsClient: mdsClient}
if err := j.refreshCreds(ctx, out, time.Now().Format(time.RFC3339)); err != nil {
t.Errorf("refreshCreds(ctx, %+v) failed unexpectedly with error: %v", out, err)
}
tests := []struct {
path string
exists bool
content string
}{
{
path: filepath.Join(link, "ca_certificates.pem"),
exists: true,
content: pem1,
},
{
path: filepath.Join(link, "certificates.pem"),
exists: true,
content: certPem,
},
{
path: filepath.Join(link, "private_key.pem"),
exists: false,
},
{
path: filepath.Join(link, "config_status"),
exists: true,
content: testConfigStatusResp,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
if test.exists {
got, err := os.ReadFile(test.path)
if err != nil {
t.Errorf("failed to read expected file %q and content %q, error: %v", test.path, test.content, err)
}
if string(got) != test.content {
t.Errorf("refreshCreds(ctx, %+v) wrote %q, want content %q", out, string(got), test.content)
}
} else {
if _, err := os.Stat(test.path); err == nil {
t.Errorf("os.Stat(%s) succeeded, want error", test.path)
}
}
})
}
}
func TestRefreshCredsError(t *testing.T) {
ctx := context.Background()
tmp := t.TempDir()
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
// Templates to use in iterations.
spiffe := "spiffe://12345.global.67890.workload.id.goog/ns/NAMESPACE_ID/sa/MANAGED_IDENTITY_ID"
domain1 := "12345.global.67890.workload.id.goog"
pem1 := "-----BEGIN CERTIFICATE-----datahere1-----END CERTIFICATE-----"
domain2 := "PEER_SPIFFE_TRUST_DOMAIN_2_IGNORE"
pem2 := "-----BEGIN CERTIFICATE-----datahere2-----END CERTIFICATE-----"
certPem := "-----BEGIN CERTIFICATE-----datahere-----END CERTIFICATE-----"
pvtPem := "-----BEGIN PRIVATE KEY-----datahere-----END PRIVATE KEY-----"
contentPrefix := filepath.Join(tmp, "workload-spiffe-contents")
tmpSymlinkPrefix := filepath.Join(tmp, "workload-spiffe-symlink")
link := filepath.Join(tmp, "workload-spiffe-credentials")
out := outputOpts{contentPrefix, tmpSymlinkPrefix, link}
client := &mdsTestClient{
spiffe: spiffe,
certPem: certPem,
pvtPem: pvtPem,
domain1: domain1,
pem1: pem1,
domain2: domain2,
pem2: pem2,
enabled: "true",
}
j := &RefresherJob{mdsClient: client}
// Run refresh creds twice. First run would succeed and second would fail.
// Verify all creds generated on the first run are present as is after failed
// second run.
for i := 1; i <= 2; i++ {
if i == 1 {
// First run should succeed.
if err := j.refreshCreds(ctx, out, fmt.Sprintf("%d", i)); err != nil {
t.Errorf("refreshCreds(ctx, %+v) failed unexpectedly with error: %v", out, err)
}
} else if i == 2 {
// Second run should fail. Fail in getting last metadata entry.
client.throwErrOn = trustAnchorsKey
if err := j.refreshCreds(ctx, out, fmt.Sprintf("%d", i)); err == nil {
t.Errorf("refreshCreds(ctx, %+v) succeeded for fake metadata error, should've failed", out)
}
}
// Verify all files are created and are still present with the content as
// expected.
tests := []struct {
path string
content string
}{
{
path: filepath.Join(link, "ca_certificates.pem"),
content: pem1,
},
{
path: filepath.Join(link, "certificates.pem"),
content: certPem,
},
{
path: filepath.Join(link, "private_key.pem"),
content: pvtPem,
},
{
path: filepath.Join(link, "config_status"),
content: testConfigStatusResp,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
got, err := os.ReadFile(test.path)
if err != nil {
t.Errorf("failed to read expected file %q and content %q with error: %v", test.path, test.content, err)
}
if string(got) != test.content {
t.Errorf("refreshCreds(ctx, %+v) wrote %q, want content %q", out, string(got), test.content)
}
})
}
// Verify the symlink was created and references the same destination
// directory.
want := fmt.Sprintf("%s-%d", contentPrefix, 1)
got, err := os.Readlink(link)
if err != nil {
t.Errorf("os.Readlink(%s) failed unexpectedly with error %v", link, err)
}
if got != want {
t.Errorf("os.Readlink(%s) = %s, want %s", link, got, want)
}
}
}
// mockWorkloadIdentityServer is a mock implementation of the WorkloadIdentityServer.
type mockWorkloadIdentityServer struct {
wipb.UnimplementedWorkloadIdentityServer
getWorkloadCertificatesErr error
getWorkloadTrustBundlesErr error
reqCount int
getWorkloadCertificatesResponse *wipb.GetWorkloadCertificatesResponse
getWorkloadTrustBundlesResponse *wipb.GetWorkloadTrustBundlesResponse
}
func (s *mockWorkloadIdentityServer) GetWorkloadCertificates(ctx context.Context, req *wipb.GetWorkloadCertificatesRequest) (*wipb.GetWorkloadCertificatesResponse, error) {
s.reqCount++
if s.getWorkloadCertificatesErr != nil {
return nil, s.getWorkloadCertificatesErr
}
return s.getWorkloadCertificatesResponse, nil
}
func (s *mockWorkloadIdentityServer) GetWorkloadTrustBundles(ctx context.Context, req *wipb.GetWorkloadTrustBundlesRequest) (*wipb.GetWorkloadTrustBundlesResponse, error) {
if s.getWorkloadTrustBundlesErr != nil {
return nil, s.getWorkloadTrustBundlesErr
}
return s.getWorkloadTrustBundlesResponse, nil
}
func startTestGRPCServer(t *testing.T, ms *mockWorkloadIdentityServer) (string, func()) {
t.Helper()
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
wipb.RegisterWorkloadIdentityServer(s, ms)
go func() {
s.Serve(lis)
}()
return lis.Addr().String(), func() {
s.Stop()
}
}
func TestIsGRPCServiceEnabled(t *testing.T) {
ctx := context.Background()
j := &RefresherJob{}
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
// Default behavior should be false as per config.
if j.isGRPCServiceEnabled(ctx) {
t.Errorf("isGRPCServiceEnabled(ctx) = true, want false")
}
config := cfg.Retrieve()
config.MWLID.Enabled = true
tests := []struct {
name string
grpcErr error
wantEnabled bool
expectedStatus Status
}{
{
name: "GRPCSuccess",
grpcErr: nil,
wantEnabled: true,
expectedStatus: ServiceAvailable,
},
{
name: "GRPCFailedPrecondition",
grpcErr: status.Error(codes.FailedPrecondition, "test error"),
wantEnabled: false,
expectedStatus: ServiceUnavailable,
},
{
name: "GRPCDeadlineExceeded",
grpcErr: status.Error(codes.DeadlineExceeded, "test error"),
wantEnabled: true,
expectedStatus: ServiceUnknown,
},
{
name: "GRPCInternalError",
grpcErr: status.Error(codes.Internal, "test error"),
wantEnabled: true,
expectedStatus: ServiceUnknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
job := &RefresherJob{}
testServer := &mockWorkloadIdentityServer{getWorkloadCertificatesErr: tt.grpcErr}
addr, stop := startTestGRPCServer(t, testServer)
defer stop()
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("failed to parse address: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("failed to parse port: %v", err)
}
config.MWLID.ServiceIP = host
config.MWLID.ServicePort = port
got := job.isGRPCServiceEnabled(ctx)
if got != tt.wantEnabled {
t.Errorf("isGRPCServiceEnabled() = %v, want %v", got, tt.wantEnabled)
}
if gotStatus := job.serverStatus(); gotStatus != tt.expectedStatus {
t.Errorf("serverStatus() = %v, want %v", gotStatus, tt.expectedStatus)
}
if tt.expectedStatus != ServiceUnknown {
// Verify that subsequent calls return without needing to re-establish
// connection.
if got := job.isGRPCServiceEnabled(ctx); got != tt.wantEnabled {
t.Errorf("isGRPCServiceEnabled() = %v, want %v", got, tt.wantEnabled)
}
if testServer.reqCount != 1 {
t.Errorf("testServer.reqCount = %d, want 1", testServer.reqCount)
}
}
})
}
}
func TestRefreshCredsWithGRPC(t *testing.T) {
ctx := context.Background()
testPrivateKey := []byte("test-private-key")
testCertChain := []byte("test-cert-chain")
testTrustBundles := []byte("test-trust-bundles")
certResp := &wipb.GetWorkloadCertificatesResponse{
CertificateChainPem: testCertChain, PrivateKeyPem: testPrivateKey,
}
noPrivateKeyResp := &wipb.GetWorkloadCertificatesResponse{
CertificateChainPem: testCertChain,
}
trustBundlesResp := &wipb.GetWorkloadTrustBundlesResponse{
SpiffeTrustBundlesMapJson: testTrustBundles,
}
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
config := cfg.Retrieve()
config.MWLID.Enabled = true
tests := []struct {
name string
mockServer *mockWorkloadIdentityServer
expectErr bool
expectedFiles map[string][]byte
}{
{
name: "Success",
mockServer: &mockWorkloadIdentityServer{
getWorkloadCertificatesResponse: certResp,
getWorkloadTrustBundlesResponse: trustBundlesResp,
},
expectErr: false,
expectedFiles: map[string][]byte{
"private_key.pem": testPrivateKey,
"certificates.pem": testCertChain,
"trust_bundles.json": testTrustBundles,
},
},
{
name: "GetWorkloadCertificatesError",
mockServer: &mockWorkloadIdentityServer{
getWorkloadCertificatesErr: status.Error(codes.Internal, "internal server error"),
},
expectErr: true,
},
{
name: "GetWorkloadTrustBundlesError",
mockServer: &mockWorkloadIdentityServer{
getWorkloadTrustBundlesErr: status.Error(codes.Internal, "internal server error"),
},
expectErr: true,
},
{
name: "EmptyPrivateKey",
mockServer: &mockWorkloadIdentityServer{
getWorkloadCertificatesResponse: noPrivateKeyResp,
getWorkloadTrustBundlesResponse: trustBundlesResp,
},
expectErr: false,
expectedFiles: map[string][]byte{
"certificates.pem": testCertChain,
"trust_bundles.json": testTrustBundles,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr, stop := startTestGRPCServer(t, tt.mockServer)
defer stop()
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("failed to parse address: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("failed to parse port: %v", err)
}
config.MWLID.ServiceIP = host
config.MWLID.ServicePort = port
now := time.Now().Format(time.RFC3339)
tmpDir := t.TempDir()
opts := outputOpts{
contentDirPrefix: filepath.Join(tmpDir, "contents"),
tempSymlinkPrefix: filepath.Join(tmpDir, "symlink"),
symlink: filepath.Join(tmpDir, "creds"),
}
job := &RefresherJob{}
err = job.refreshCreds(ctx, opts, now)
if (err != nil) != tt.expectErr {
t.Errorf("refreshCreds(ctx, %+v, %s) error = %v, expectErr %v", opts, now, err, tt.expectErr)
return
}
if !tt.expectErr {
entries, err := os.ReadDir(opts.symlink)
if err != nil {
t.Errorf("os.ReadDir(%s) failed unexpectedly with error: %v", opts.symlink, err)
return
}
if len(entries) != len(tt.expectedFiles) {
t.Errorf("os.ReadDir(%s) = %d entries, want %d", opts.symlink, len(entries), len(tt.expectedFiles))
return
}
for filename, expectedContent := range tt.expectedFiles {
filePath := filepath.Join(opts.symlink, filename)
gotContent, err := os.ReadFile(filePath)
if err != nil {
t.Errorf("os.ReadFile(%s) failed unexpectedly with error: %v", filePath, err)
continue
}
if string(gotContent) != string(expectedContent) {
t.Errorf("os.ReadFile(%s) = %s, want %s", filePath, string(gotContent), string(expectedContent))
}
}
}
})
}
}
func TestNewClient(t *testing.T) {
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
job := &RefresherJob{}
ctx := context.Background()
client1, err := job.newClient(ctx)
if err != nil {
t.Fatalf("job.newClient(ctx) failed unexpectedly with error: %v", err)
}
if client1 == nil {
t.Fatal("job.newClient(ctx) = nil, want non-nil client")
}
if job.grpcClient != client1 {
t.Errorf("job.newClient(ctx) did not cache client")
}
// Second call should return the cached client.
client2, err := job.newClient(ctx)
if err != nil {
t.Fatalf("job.newClient(ctx) failed unexpectedly with error on second call: %v", err)
}
if client1 != client2 {
t.Error("job.newClient(ctx) should return a cached client on second call, but it created a new one")
}
if job.grpcClient != client1 {
t.Errorf("job.newClient(ctx) reset cached client on second call")
}
}
func TestRefreshCredsSkip(t *testing.T) {
ctx := context.Background()
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
addr, stop := startTestGRPCServer(t, &mockWorkloadIdentityServer{getWorkloadCertificatesErr: status.Error(codes.FailedPrecondition, "failed precondition")})
defer stop()
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("failed to parse address: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("failed to parse port: %v", err)
}
config := cfg.Retrieve()
config.MWLID.ServiceIP = host
config.MWLID.ServicePort = port
mdsClient := &mdsTestClient{enabled: "false"}
j := &RefresherJob{mdsClient: mdsClient}
tmpDir := t.TempDir()
opts := outputOpts{
contentDirPrefix: filepath.Join(tmpDir, "contents"),
tempSymlinkPrefix: filepath.Join(tmpDir, "symlink"),
symlink: filepath.Join(tmpDir, "creds"),
}
if err := j.refreshCreds(ctx, opts, "test"); err != nil {
t.Errorf("refreshCreds(ctx, opts, %s) = error %v, want nil", "test", err)
}
for _, f := range []string{opts.symlink, opts.tempSymlinkPrefix, opts.contentDirPrefix} {
if file.Exists(f, file.TypeDir) {
t.Errorf("refreshCreds(ctx, %+v, %s) created directory %q, expected to be no-op", opts, "test", f)
}
}
}
func TestCloseClient(t *testing.T) {
job := &RefresherJob{}
if err := cfg.Load(nil); err != nil {
t.Fatalf("cfg.Load() failed unexpectedly with error: %v", err)
}
_, err := job.newClient(context.Background())
if err != nil {
t.Fatalf("job.newClient(ctx) failed unexpectedly with error: %v", err)
}
job.closeClient()
if job.grpcClient != nil {
t.Errorf("job.closeClient() did not close the client")
}
// Verify that subsequent calls to closeClient don't panic when client is nil.
job.closeClient()
}
type fakeMDSClient struct {
counter int
get func(context.Context) (*metadata.Descriptor, error)
getKey func(context.Context, string, map[string]string) (string, error)
getKeyRecursive func(context.Context, string) (string, error)
watch func(context.Context) (*metadata.Descriptor, error)
writeGuestAttributes func(context.Context, string, string) error
}
func (f *fakeMDSClient) Get(ctx context.Context) (*metadata.Descriptor, error) {
if f.get == nil {
return &metadata.Descriptor{}, nil
}
return f.get(ctx)
}
func (f *fakeMDSClient) GetKey(ctx context.Context, key string, headers map[string]string) (string, error) {
if f.getKey == nil {
return "", nil
}
return f.getKey(ctx, key, headers)
}
func (f *fakeMDSClient) GetKeyRecursive(ctx context.Context, key string) (string, error) {
if f.getKeyRecursive == nil {
return "", nil
}
return f.getKeyRecursive(ctx, key)
}
func (f *fakeMDSClient) Watch(ctx context.Context) (*metadata.Descriptor, error) {
if f.watch == nil {
return &metadata.Descriptor{}, nil
}
return f.watch(ctx)
}
func (f *fakeMDSClient) WriteGuestAttributes(ctx context.Context, key, value string) error {
if f.writeGuestAttributes == nil {
return nil
}
return f.writeGuestAttributes(ctx, key, value)
}
func TestWatchIdentity(t *testing.T) {
mdsIdentity1 := `{
"instance": {
"identity-configuration": {
"identity-uuid": "identity-1"
}
}
}`
mdsIdentity2 := `{
"instance": {
"identity-configuration": {
"identity-uuid": "identity-2"
}
}
}`
emptyMds := `{}`
tests := []struct {
name string
prevMdsJSON string
currMdsJSON string
// watchError is the error that will be returned by the mock MDS client.
watchError error
// expectChannel is true if the channel should send something; false otherwise.
expectChannel bool
}{
{
name: "identity_changed",
prevMdsJSON: mdsIdentity1,
currMdsJSON: mdsIdentity2,
watchError: nil,
expectChannel: true,
},
{
name: "identity_same",
prevMdsJSON: mdsIdentity1,
currMdsJSON: mdsIdentity1,
watchError: nil,
expectChannel: false,
},
{
name: "no_identity",
prevMdsJSON: emptyMds,
currMdsJSON: emptyMds,
watchError: nil,
expectChannel: false,
},
{
name: "watch_error",
watchError: errors.New("test error"),
expectChannel: false,
},
{
name: "empty_mds_after_identity_changed",
prevMdsJSON: mdsIdentity1,
currMdsJSON: emptyMds,
watchError: nil,
expectChannel: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
// Setup the mock MDS client.
mdsClient := &fakeMDSClient{}
mdsClient.watch = func(context.Context) (*metadata.Descriptor, error) {
if mdsClient.counter == 0 {
if tc.watchError != nil {
return nil, tc.watchError
}
mdsClient.counter++
return metadata.UnmarshalDescriptor(tc.prevMdsJSON)
}
return metadata.UnmarshalDescriptor(tc.currMdsJSON)
}
// Setup the refresher job.
job := &RefresherJob{
mdsClient: mdsClient,
}
// Get the mds channel from watching identity.
mdsChan := job.watchIdentity(ctx)
// Sleep for a short time to ensure the refresher has time to run.
time.Sleep(200 * time.Millisecond)
// Try receiving from the channel.
select {
case <-mdsChan:
if !tc.expectChannel {
t.Errorf("watchIdentity(ctx) sent something on the channel, want nothing")
}
default:
if tc.expectChannel {
t.Errorf("watchIdentity(ctx) did not send anything on the channel, want something")
}
}
// Try receiving again from the channel. This should not receive anything.
select {
case <-mdsChan:
t.Errorf("watchIdentity(ctx) sent something on the channel, want nothing")
default:
// This is expected.
}
})
}
}