metadata.go (112 lines of code) (raw):
// Copyright 2025 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 client
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"cloud.google.com/go/compute/metadata"
)
const (
identityTokenPath = "instance/service-accounts/default/identity?audience=agentcommunication.googleapis.com&format=full"
)
type cachedValue struct {
k string
v string
parser func(string) string
sync.Mutex
}
var (
zone = &cachedValue{k: "instance/zone", parser: func(v string) string { return v[strings.LastIndex(v, "/")+1:] }}
projNum = &cachedValue{k: "project/numeric-project-id"}
instID = &cachedValue{k: "instance/id"}
idToken = &cachedIDToken{}
)
func (c *cachedValue) get() (string, error) {
c.Lock()
defer c.Unlock()
if c.v != "" {
return c.v, nil
}
var err error
c.v, err = metadata.Get(c.k)
if err != nil {
return "", err
}
if c.parser != nil {
c.v = c.parser(c.v)
}
return c.v, nil
}
type claimSet struct {
Exp int64 `json:"exp"` // this is all we are interested in
}
func decodeTokenExpiry(payload string) (int64, error) {
// decode returned id token to get expiry
s := strings.Split(payload, ".")
if len(s) < 2 {
return 0, errors.New("invalid token received")
}
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return 0, err
}
c := &claimSet{}
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c)
return c.Exp, err
}
type cachedIDToken struct {
expTime *time.Time
raw string
sync.Mutex
}
func (t *cachedIDToken) get() error {
data, err := metadata.Get(identityTokenPath)
if err != nil {
return fmt.Errorf("error getting token from metadata: %w", err)
}
exp, err := decodeTokenExpiry(data)
if err != nil {
return err
}
t.raw = data
expTime := time.Unix(exp, 0)
t.expTime = &expTime
return nil
}
func getIdentityToken() (string, error) {
idToken.Lock()
defer idToken.Unlock()
// Re-request token if expiry is within 10 minutes.
if idToken.expTime == nil || time.Now().After(idToken.expTime.Add(-10*time.Minute)) {
if err := idToken.get(); err != nil {
return "", err
}
}
return idToken.raw, nil
}
func getZone() (string, error) {
return zone.get()
}
func getProjectNumber() (string, error) {
return projNum.get()
}
func getInstanceID() (string, error) {
return instID.get()
}
func getResourceID() (string, error) {
zone, err := getZone()
if err != nil {
return "", err
}
projectNum, err := getProjectNumber()
if err != nil {
return "", err
}
instanceID, err := getInstanceID()
if err != nil {
return "", err
}
return fmt.Sprintf("projects/%s/zones/%s/instances/%s", projectNum, zone, instanceID), nil
}