blob: b97b4deb3960af1ea4475c43c53f6e42254b05f5 [file] [log] [blame]
// Package skmetadata provides helper functions to implement the meta data server for the Skolo.
package skmetadata
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metadata"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"golang.org/x/oauth2"
compute "google.golang.org/api/compute/v1"
)
// ProjectMetadata is an interface which supports retrieval of project-level
// metadata values by key.
type ProjectMetadata interface {
Get(string) (string, error)
}
// InstanceMetadata is an interface which supports retrieval of instance-level
// metadata values by instance name and key.
type InstanceMetadata interface {
Get(string, string) (string, error)
}
// ValidateToken returns an error if the given token is not valid.
func ValidateToken(tok *oauth2.Token) error {
if util.TimeIsZero(tok.Expiry) {
return fmt.Errorf("Token has no expiration!")
}
now := time.Now()
if now.After(tok.Expiry) {
// This case is covered by tok.Valid(), but we want to provide a
// better error message.
return fmt.Errorf("Token is expired! Expiry: %s; time is now %s.", tok.Expiry, now)
}
if !tok.Valid() {
return fmt.Errorf("Token is invalid!")
}
return nil
}
// ServiceAccountToken is a struct used for caching an access token for a
// service account.
type ServiceAccountToken struct {
filename string
tok *oauth2.Token
mtx sync.RWMutex
updateFn func() (*oauth2.Token, error)
tokenSrc oauth2.TokenSource
}
// TODO(stephana): Once this version works in the Skolo, remove the isKeyFile option below and
// get rid of the old implementation of meta_data_server.
// NewServiceAccountToken returns a ServiceAccountToken based on the contents
// of the given file.
// If 'isKeyFile' is true then the given file is assumed to be the keyfile of a service account
// and it is used to to retrieve short-lived tokens continuously.
// If 'isKeyFile' is false the given file is assumed to contain the token
// (updated by another process) and it will be loaded continuously.
func NewServiceAccountToken(fp string, isKeyFile bool) (*ServiceAccountToken, error) {
rv := &ServiceAccountToken{
filename: fp,
}
// Set the update function whether the provided file contains a cached token
// or a service account keyfile.
rv.updateFn = rv.readTokenFromFile
if isKeyFile {
var err error
rv.tokenSrc, err = auth.NewJWTServiceAccountTokenSource("#bogus", fp, compute.CloudPlatformScope, auth.SCOPE_USERINFO_EMAIL)
if err != nil {
return nil, err
}
rv.updateFn = rv.tokenSrc.Token
}
return rv, rv.Update()
}
// UpdateFromFile updates the ServiceAccountToken from the given file.
func (t *ServiceAccountToken) Update() error {
tok, err := t.updateFn()
if err != nil {
return err
}
// Update the stored token.
t.mtx.Lock()
defer t.mtx.Unlock()
t.tok = tok
sklog.Infof("Updated token: %s", tok.AccessToken[len(tok.AccessToken)-8:])
return nil
}
// readTokenFromFile opens the file provided to the constructor and reads a token from it.
func (t *ServiceAccountToken) readTokenFromFile() (*oauth2.Token, error) {
// Read the token from the file.
contents, err := ioutil.ReadFile(t.filename)
if err != nil {
return nil, err
}
tok := new(oauth2.Token)
if err := json.NewDecoder(bytes.NewReader(contents)).Decode(tok); err != nil {
return nil, err
}
// Validate the token.
if err := ValidateToken(tok); err != nil {
return nil, err
}
return tok, nil
}
// Get returns the current value of the access token.
func (t *ServiceAccountToken) Get() (*oauth2.Token, error) {
t.mtx.RLock()
defer t.mtx.RUnlock()
return t.tok, nil
}
// UpdateLoop updates the ServiceAccountToken from the given file on a timer.
func (t *ServiceAccountToken) UpdateLoop(ctx context.Context) {
// get_oauth2_token runs every 45 minutes, and the tokens are valid for
// 60 minutes. Reloading the token every 10 minutes ensures that our
// token is always valid.
util.RepeatCtx(ctx, 10*time.Minute, func(ctx context.Context) {
if err := t.Update(); err != nil {
sklog.Errorf("Failed to update ServiceAccountToken from file: %s", err)
}
})
}
// makeInstanceMetadataHandler returns an HTTP handler func which serves
// instance-level metadata.
func makeInstanceMetadataHandler(im InstanceMetadata) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
instance := r.RemoteAddr // TODO(borenet): This is not correct.
key, ok := mux.Vars(r)["key"]
if !ok {
httputils.ReportError(w, nil, "Metadata key is required.", http.StatusInternalServerError)
}
sklog.Infof("Instance metadata: %s", key)
val, err := im.Get(instance, key)
if err != nil {
http.NotFound(w, r)
return
}
if _, err := w.Write([]byte(val)); err != nil {
httputils.ReportError(w, nil, "Failed to write response.", http.StatusInternalServerError)
return
}
}
}
// makeProjectMetadataHandler returns an HTTP handler func which serves
// project-level metadata.
func makeProjectMetadataHandler(pm ProjectMetadata) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
key, ok := mux.Vars(r)["key"]
if !ok {
httputils.ReportError(w, nil, "Metadata key is required.", http.StatusInternalServerError)
}
sklog.Infof("Project metadata: %s", key)
val, err := pm.Get(key)
if err != nil {
http.NotFound(w, r)
return
}
if _, err := w.Write([]byte(val)); err != nil {
httputils.ReportError(w, nil, "Failed to write response.", http.StatusInternalServerError)
return
}
}
}
// mdHandler adds a handler to the given router for the specified metadata endpoint.
func mdHandler(r *mux.Router, level string, handler http.HandlerFunc) {
path := fmt.Sprintf(metadata.METADATA_SUB_URL_TMPL, level, "{key}")
r.HandleFunc(path, handler).Headers(metadata.HEADER_MD_FLAVOR_KEY, metadata.HEADER_MD_FLAVOR_VAL)
sklog.Infof("%s: %s", level, path)
}
// retrieve this server's IP address(es).
func getMyIP() ([]string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
rv := []string{}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
rv = append(rv, ip.String())
}
}
return rv, nil
}
// SetupServer adds handlers to the given router which mimic the API of the GCE
// metadata server.
func SetupServer(r *mux.Router, pm ProjectMetadata, im InstanceMetadata, tokenMapping map[string]*ServiceAccountToken) {
mdHandler(r, metadata.LEVEL_INSTANCE, makeInstanceMetadataHandler(im))
mdHandler(r, metadata.LEVEL_PROJECT, makeProjectMetadataHandler(pm))
myIpAddrs, err := getMyIP()
if err != nil {
sklog.Fatal(err)
}
// The service account token path does not quite follow the pattern of
// the other two metadata types.
r.HandleFunc(metadata.TOKEN_PATH, func(w http.ResponseWriter, r *http.Request) {
// Find the token for this requester.
ipAddr := strings.Split(r.RemoteAddr, ":")[0]
var tok *ServiceAccountToken
if t, ok := tokenMapping[ipAddr]; ok {
// 1. We have a token specifically for this IP address.
tok = t
} else if t, ok := tokenMapping["self"]; ok && util.In(ipAddr, myIpAddrs) {
// 2. The request is coming from this machine, and we
// have a token specifically for that case.
tok = t
} else if t, ok := tokenMapping["*"]; ok {
// 3. We don't know about this IP address specifically,
// but we have a default token.
tok = t
} else {
// 4. None of the above. Return an error.
httputils.ReportError(w, fmt.Errorf("Unknown IP address %s and no default token provided.", ipAddr), "Failed to retrieve token.", http.StatusInternalServerError)
return
}
t, err := tok.Get()
if err != nil {
httputils.ReportError(w, err, "Failed to obtain key.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
// Copied from
// https://github.com/golang/oauth2/blob/f6093e37b6cb4092101a298aba5d794eb570757f/google/google.go#L185
res := struct {
AccessToken string `json:"access_token"`
ExpiresInSec int `json:"expires_in"`
TokenType string `json:"token_type"`
}{
AccessToken: t.AccessToken,
ExpiresInSec: int(t.Expiry.Sub(time.Now()).Seconds()),
TokenType: t.TokenType,
}
sklog.Infof("Token requested by %s, serving %s", r.RemoteAddr, res.AccessToken[len(res.AccessToken)-8:])
if err := json.NewEncoder(w).Encode(res); err != nil {
httputils.ReportError(w, err, "Failed to write response.", http.StatusInternalServerError)
return
}
})
}