mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-19 22:44:18 +00:00
Add CLI caching of cluster-specific credentials.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package execcredcache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
// errUnsupportedVersion is returned (internally) when we encounter a version of the cache file that we
|
||||
// don't understand how to handle (such as one produced by a future version of Pinniped).
|
||||
errUnsupportedVersion = fmt.Errorf("unsupported credential cache version")
|
||||
)
|
||||
|
||||
const (
|
||||
// apiVersion is the Kubernetes-style API version of the credential cache file object.
|
||||
apiVersion = "config.supervisor.pinniped.dev/v1alpha1"
|
||||
|
||||
// apiKind is the Kubernetes-style Kind of the credential cache file object.
|
||||
apiKind = "CredentialCache"
|
||||
|
||||
// maxCacheDuration is how long a credential can remain in the cache even if it's still otherwise valid.
|
||||
maxCacheDuration = 1 * time.Hour
|
||||
)
|
||||
|
||||
type (
|
||||
// credCache is the object which is YAML-serialized to form the contents of the cache file.
|
||||
credCache struct {
|
||||
metav1.TypeMeta
|
||||
Entries []entry `json:"credentials"`
|
||||
}
|
||||
|
||||
// entry is a single credential in the cache file.
|
||||
entry struct {
|
||||
Key string `json:"key"`
|
||||
CreationTimestamp metav1.Time `json:"creationTimestamp"`
|
||||
LastUsedTimestamp metav1.Time `json:"lastUsedTimestamp"`
|
||||
Credential *clientauthenticationv1beta1.ExecCredentialStatus `json:"credential"`
|
||||
}
|
||||
)
|
||||
|
||||
// readCache loads a credCache from a path on disk. If the requested path does not exist, it returns an empty cache.
|
||||
func readCache(path string) (*credCache, error) {
|
||||
cacheYAML, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// If the file was not found, generate a freshly initialized empty cache.
|
||||
return emptyCache(), nil
|
||||
}
|
||||
// Otherwise bubble up the error.
|
||||
return nil, fmt.Errorf("could not read cache file: %w", err)
|
||||
}
|
||||
|
||||
// If we read the file successfully, unmarshal it from YAML.
|
||||
var cache credCache
|
||||
if err := yaml.Unmarshal(cacheYAML, &cache); err != nil {
|
||||
return nil, fmt.Errorf("invalid cache file: %w", err)
|
||||
}
|
||||
|
||||
// Validate that we're reading a version of the config we understand how to parse.
|
||||
if !(cache.TypeMeta.APIVersion == apiVersion && cache.TypeMeta.Kind == apiKind) {
|
||||
return nil, fmt.Errorf("%w: %#v", errUnsupportedVersion, cache.TypeMeta)
|
||||
}
|
||||
return &cache, nil
|
||||
}
|
||||
|
||||
// emptyCache returns an empty, initialized credCache.
|
||||
func emptyCache() *credCache {
|
||||
return &credCache{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: apiVersion, Kind: apiKind},
|
||||
Entries: make([]entry, 0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// writeTo writes the cache to the specified file path.
|
||||
func (c *credCache) writeTo(path string) error {
|
||||
// Marshal the cache back to YAML and save it to the file.
|
||||
cacheYAML, err := yaml.Marshal(c)
|
||||
if err == nil {
|
||||
err = ioutil.WriteFile(path, cacheYAML, 0600)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// normalized returns a copy of the credCache with stale entries removed and entries sorted in a canonical order.
|
||||
func (c *credCache) normalized() *credCache {
|
||||
result := emptyCache()
|
||||
|
||||
// Clean up expired/invalid tokens.
|
||||
now := time.Now()
|
||||
result.Entries = make([]entry, 0, len(c.Entries))
|
||||
|
||||
for _, e := range c.Entries {
|
||||
// Eliminate any cache entries that are missing a credential or an expiration timestamp.
|
||||
if e.Credential == nil || e.Credential.ExpirationTimestamp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Eliminate any expired credentials.
|
||||
if e.Credential.ExpirationTimestamp.Time.Before(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Eliminate any entries older than maxCacheDuration.
|
||||
if e.CreationTimestamp.Time.Before(now.Add(-maxCacheDuration)) {
|
||||
continue
|
||||
}
|
||||
result.Entries = append(result.Entries, e)
|
||||
}
|
||||
|
||||
// Sort the entries by creation time.
|
||||
sort.SliceStable(result.Entries, func(i, j int) bool {
|
||||
return result.Entries[i].CreationTimestamp.Before(&result.Entries[j].CreationTimestamp)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user