initial commit

This commit is contained in:
Eric Chiang
2016-07-25 13:00:28 -07:00
commit cab271f304
1438 changed files with 335968 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
package kubernetes
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/gtank/cryptopasta"
yaml "gopkg.in/yaml.v2"
"github.com/coreos/poke/storage"
"github.com/coreos/poke/storage/kubernetes/k8sapi"
)
type client struct {
client *http.Client
baseURL string
namespace string
apiVersion string
now func() time.Time
// BUG: currently each third party API group can only have one resource in it,
// so for each resource this storage uses, it need a unique API group.
//
// Prepend the name of each resource to the API group for a predictable mapping.
//
// See: https://github.com/kubernetes/kubernetes/pull/28414
prependResourceNameToAPIGroup bool
}
func (c *client) apiVersionForResource(resource string) string {
if !c.prependResourceNameToAPIGroup {
return c.apiVersion
}
return resource + "." + c.apiVersion
}
func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
basePath := "apis/"
if apiVersion == "v1" {
basePath = "api/"
}
if c.prependResourceNameToAPIGroup && apiVersion != "" && resource != "" {
apiVersion = resource + "." + apiVersion
}
var p string
if namespace != "" {
p = path.Join(basePath, apiVersion, "namespaces", namespace, resource, name)
} else {
p = path.Join(basePath, apiVersion, resource, name)
}
if strings.HasSuffix(c.baseURL, "/") {
return c.baseURL + p
}
return c.baseURL + "/" + p
}
type httpErr struct {
method string
url string
status string
body []byte
}
func (e *httpErr) Error() string {
return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, e.status, bytes.TrimSpace(e.body))
}
func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
for _, status := range validStatusCodes {
if r.StatusCode == status {
return nil
}
}
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 2<<15)) // 64 KiB
if err != nil {
return fmt.Errorf("read response body: %v", err)
}
var url, method string
if r.Request != nil {
method = r.Request.Method
url = r.Request.URL.String()
}
err = &httpErr{method, url, r.Status, body}
log.Printf("%s", err)
if r.StatusCode == http.StatusNotFound {
return storage.ErrNotFound
}
return err
}
// Close the response body. The initial request is drained so the connection can
// be reused.
func closeResp(r *http.Response) {
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
}
func (c *client) get(resource, name string, v interface{}) error {
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
resp, err := c.client.Get(url)
if err != nil {
return err
}
defer closeResp(resp)
if err := checkHTTPErr(resp, http.StatusOK); err != nil {
return err
}
return json.NewDecoder(resp.Body).Decode(v)
}
func (c *client) list(resource string, v interface{}) error {
return c.get(resource, "", v)
}
func (c *client) post(resource string, v interface{}) error {
body, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("marshal object: %v", err)
}
url := c.urlFor(c.apiVersion, c.namespace, resource, "")
resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer closeResp(resp)
return checkHTTPErr(resp, http.StatusCreated)
}
func (c *client) delete(resource, name string) error {
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("create delete request: %v", err)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("delete request: %v", err)
}
defer closeResp(resp)
return checkHTTPErr(resp, http.StatusOK)
}
func (c *client) put(resource, name string, v interface{}) error {
body, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("marshal object: %v", err)
}
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create patch request: %v", err)
}
req.Header.Set("Content-Length", strconv.Itoa(len(body)))
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("patch request: %v", err)
}
defer closeResp(resp)
return checkHTTPErr(resp, http.StatusOK)
}
func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (*client, error) {
tlsConfig := cryptopasta.DefaultTLSConfig()
data := func(b []byte, file string) ([]byte, error) {
if b != nil {
return b, nil
}
if file == "" {
return nil, nil
}
return ioutil.ReadFile(file)
}
if caData, err := data(cluster.CertificateAuthorityData, cluster.CertificateAuthority); err != nil {
return nil, err
} else if caData != nil {
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM(caData) {
return nil, fmt.Errorf("no certificate data found: %v", err)
}
}
clientCert, err := data(user.ClientCertificateData, user.ClientCertificate)
if err != nil {
return nil, err
}
clientKey, err := data(user.ClientKeyData, user.ClientKey)
if err != nil {
return nil, err
}
if clientCert != nil && clientKey != nil {
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
return nil, fmt.Errorf("failed to load client cert: %v", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
var t http.RoundTripper = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSClientConfig: tlsConfig,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
if user.Token != "" {
t = transport{
updateReq: func(r *http.Request) {
r.Header.Set("Authorization", "Bearer "+user.Token)
},
base: t,
}
}
if user.Username != "" && user.Password != "" {
t = transport{
updateReq: func(r *http.Request) {
r.SetBasicAuth(user.Username, user.Password)
},
base: t,
}
}
// TODO(ericchiang): make API Group and version configurable.
return &client{&http.Client{Transport: t}, cluster.Server, namespace, "oidc.coreos.com/v1", time.Now, true}, nil
}
type transport struct {
updateReq func(r *http.Request)
base http.RoundTripper
}
func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
t.updateReq(r2)
return t.base.RoundTrip(r2)
}
func loadKubeConfig(kubeConfigPath string) (cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, err error) {
data, err := ioutil.ReadFile(kubeConfigPath)
if err != nil {
err = fmt.Errorf("read %s: %v", kubeConfigPath, err)
return
}
var c k8sapi.Config
if err = yaml.Unmarshal(data, &c); err != nil {
err = fmt.Errorf("unmarshal %s: %v", kubeConfigPath, err)
return
}
cluster, user, namespace, err = currentContext(&c)
if namespace == "" {
namespace = "default"
}
return
}
func inClusterConfig() (cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, err error) {
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
if len(host) == 0 || len(port) == 0 {
err = fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
return
}
cluster = k8sapi.Cluster{
Server: "https://" + host + ":" + port,
CertificateAuthority: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
}
if namespace = os.Getenv("KUBERNETES_POD_NAMESPACE"); namespace == "" {
err = fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_POD_NAMESPACE must be defined")
return
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return
}
user = k8sapi.AuthInfo{Token: string(token)}
return
}
func currentContext(config *k8sapi.Config) (cluster k8sapi.Cluster, user k8sapi.AuthInfo, ns string, err error) {
if config.CurrentContext == "" {
return cluster, user, "", errors.New("kubeconfig has no current context")
}
context, ok := func() (k8sapi.Context, bool) {
for _, namedContext := range config.Contexts {
if namedContext.Name == config.CurrentContext {
return namedContext.Context, true
}
}
return k8sapi.Context{}, false
}()
if !ok {
return cluster, user, "", fmt.Errorf("no context named %q found", config.CurrentContext)
}
cluster, ok = func() (k8sapi.Cluster, bool) {
for _, namedCluster := range config.Clusters {
if namedCluster.Name == context.Cluster {
return namedCluster.Cluster, true
}
}
return k8sapi.Cluster{}, false
}()
if !ok {
return cluster, user, "", fmt.Errorf("no cluster named %q found", context.Cluster)
}
user, ok = func() (k8sapi.AuthInfo, bool) {
for _, namedAuthInfo := range config.AuthInfos {
if namedAuthInfo.Name == context.AuthInfo {
return namedAuthInfo.AuthInfo, true
}
}
return k8sapi.AuthInfo{}, false
}()
if !ok {
return cluster, user, "", fmt.Errorf("no user named %q found", context.AuthInfo)
}
return cluster, user, context.Namespace, nil
}
func newInClusterClient() (*client, error) {
return nil, nil
}

View File

@@ -0,0 +1,2 @@
// Package kubernetes provides a storage interface using Kubernetes third party APIs.
package kubernetes

View File

@@ -0,0 +1,29 @@
package kubernetes
import (
"fmt"
"log"
)
// TODO(ericchiang): Complete this.
type multiErr []error
func (m multiErr) Error() string {
return fmt.Sprintf("errors encountered: %s", m)
}
func (cli *client) gcAuthRequests() error {
var authRequests AuthRequestList
if err := cli.list(resourceAuthRequest, &authRequests); err != nil {
return err
}
for _, authRequest := range authRequests.AuthRequests {
if cli.now().After(authRequest.Expiry) {
if err := cli.delete(resourceAuthRequest, authRequest.ObjectMeta.Name); err != nil {
log.Printf("failed to detele auth request: %v", err)
}
}
}
return nil
}

View File

@@ -0,0 +1,140 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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
http://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 k8sapi
// Where possible, json tags match the cli argument names.
// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted.
// Config holds the information needed to build connect to remote kubernetes clusters as a given user
type Config struct {
// Legacy field from pkg/api/types.go TypeMeta.
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
Kind string `yaml:"kind,omitempty"`
// DEPRECATED: APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
// Because a cluster can run multiple API groups and potentially multiple versions of each, it no longer makes sense to specify
// a single value for the cluster version.
// This field isn't really needed anyway, so we are deprecating it without replacement.
// It will be ignored if it is present.
APIVersion string `yaml:"apiVersion,omitempty"`
// Preferences holds general information to be use for cli interactions
Preferences Preferences `yaml:"preferences"`
// Clusters is a map of referencable names to cluster configs
Clusters []NamedCluster `yaml:"clusters"`
// AuthInfos is a map of referencable names to user configs
AuthInfos []NamedAuthInfo `yaml:"users"`
// Contexts is a map of referencable names to context configs
Contexts []NamedContext `yaml:"contexts"`
// CurrentContext is the name of the context that you would like to use by default
CurrentContext string `yaml:"current-context"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
Extensions []NamedExtension `yaml:"extensions,omitempty"`
}
// Preferences contains information about the users command line experience preferences.
type Preferences struct {
Colors bool `yaml:"colors,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
Extensions []NamedExtension `yaml:"extensions,omitempty"`
}
// Cluster contains information about how to communicate with a kubernetes cluster
type Cluster struct {
// Server is the address of the kubernetes cluster (https://hostname:port).
Server string `yaml:"server"`
// APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
APIVersion string `yaml:"api-version,omitempty"`
// InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"`
// CertificateAuthority is the path to a cert file for the certificate authority.
CertificateAuthority string `yaml:"certificate-authority,omitempty"`
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
CertificateAuthorityData []byte `yaml:"certificate-authority-data,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
Extensions []NamedExtension `yaml:"extensions,omitempty"`
}
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
type AuthInfo struct {
// ClientCertificate is the path to a client cert file for TLS.
ClientCertificate string `yaml:"client-certificate,omitempty"`
// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
ClientCertificateData []byte `yaml:"client-certificate-data,omitempty"`
// ClientKey is the path to a client key file for TLS.
ClientKey string `yaml:"client-key,omitempty"`
// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
ClientKeyData []byte `yaml:"client-key-data,omitempty"`
// Token is the bearer token for authentication to the kubernetes cluster.
Token string `yaml:"token,omitempty"`
// Impersonate is the username to imperonate. The name matches the flag.
Impersonate string `yaml:"as,omitempty"`
// Username is the username for basic authentication to the kubernetes cluster.
Username string `yaml:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
Password string `yaml:"password,omitempty"`
// AuthProvider specifies a custom authentication plugin for the kubernetes cluster.
AuthProvider *AuthProviderConfig `yaml:"auth-provider,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
Extensions []NamedExtension `yaml:"extensions,omitempty"`
}
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
type Context struct {
// Cluster is the name of the cluster for this context
Cluster string `yaml:"cluster"`
// AuthInfo is the name of the authInfo for this context
AuthInfo string `yaml:"user"`
// Namespace is the default namespace to use on unspecified requests
Namespace string `yaml:"namespace,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
Extensions []NamedExtension `yaml:"extensions,omitempty"`
}
// NamedCluster relates nicknames to cluster information
type NamedCluster struct {
// Name is the nickname for this Cluster
Name string `yaml:"name"`
// Cluster holds the cluster information
Cluster Cluster `yaml:"cluster"`
}
// NamedContext relates nicknames to context information
type NamedContext struct {
// Name is the nickname for this Context
Name string `yaml:"name"`
// Context holds the context information
Context Context `yaml:"context"`
}
// NamedAuthInfo relates nicknames to auth information
type NamedAuthInfo struct {
// Name is the nickname for this AuthInfo
Name string `yaml:"name"`
// AuthInfo holds the auth information
AuthInfo AuthInfo `yaml:"user"`
}
// NamedExtension relates nicknames to extension information
type NamedExtension struct {
// Name is the nickname for this Extension
Name string `yaml:"name"`
}
// AuthProviderConfig holds the configuration for a specified auth provider.
type AuthProviderConfig struct {
Name string `yaml:"name"`
Config map[string]string `yaml:"config"`
}

View File

@@ -0,0 +1,2 @@
// Package k8sapi holds vendored Kubernetes types.
package k8sapi

View File

@@ -0,0 +1,49 @@
/*
Copyright 2015 The Kubernetes Authors.
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
http://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 k8sapi
// A ThirdPartyResource is a generic representation of a resource, it is used by add-ons and plugins to add new resource
// types to the API. It consists of one or more Versions of the api.
type ThirdPartyResource struct {
TypeMeta `json:",inline"`
// Standard object metadata
ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Description is the description of this object.
Description string `json:"description,omitempty" protobuf:"bytes,2,opt,name=description"`
// Versions are versions for this third party object
Versions []APIVersion `json:"versions,omitempty" protobuf:"bytes,3,rep,name=versions"`
}
// ThirdPartyResourceList is a list of ThirdPartyResources.
type ThirdPartyResourceList struct {
TypeMeta `json:",inline"`
// Standard list metadata.
ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Items is the list of ThirdPartyResources.
Items []ThirdPartyResource `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// An APIVersion represents a single concrete version of an object model.
type APIVersion struct {
// Name of this version (e.g. 'v1').
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
}

View File

@@ -0,0 +1,138 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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
http://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 k8sapi
import (
"encoding/json"
"time"
)
// Time is a wrapper around time.Time which supports correct
// marshaling to YAML and JSON. Wrappers are provided for many
// of the factory methods that the time package offers.
//
// +protobuf.options.marshal=false
// +protobuf.as=Timestamp
type Time struct {
time.Time `protobuf:"-"`
}
// NewTime returns a wrapped instance of the provided time
func NewTime(time time.Time) Time {
return Time{time}
}
// Date returns the Time corresponding to the supplied parameters
// by wrapping time.Date.
func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time {
return Time{time.Date(year, month, day, hour, min, sec, nsec, loc)}
}
// Now returns the current local time.
func Now() Time {
return Time{time.Now()}
}
// IsZero returns true if the value is nil or time is zero.
func (t *Time) IsZero() bool {
if t == nil {
return true
}
return t.Time.IsZero()
}
// Before reports whether the time instant t is before u.
func (t Time) Before(u Time) bool {
return t.Time.Before(u.Time)
}
// Equal reports whether the time instant t is equal to u.
func (t Time) Equal(u Time) bool {
return t.Time.Equal(u.Time)
}
// Unix returns the local time corresponding to the given Unix time
// by wrapping time.Unix.
func Unix(sec int64, nsec int64) Time {
return Time{time.Unix(sec, nsec)}
}
// Rfc3339Copy returns a copy of the Time at second-level precision.
func (t Time) Rfc3339Copy() Time {
copied, _ := time.Parse(time.RFC3339, t.Format(time.RFC3339))
return Time{copied}
}
// UnmarshalJSON implements the json.Unmarshaller interface.
func (t *Time) UnmarshalJSON(b []byte) error {
if len(b) == 4 && string(b) == "null" {
t.Time = time.Time{}
return nil
}
var str string
json.Unmarshal(b, &str)
pt, err := time.Parse(time.RFC3339, str)
if err != nil {
return err
}
t.Time = pt.Local()
return nil
}
// UnmarshalQueryParameter converts from a URL query parameter value to an object
func (t *Time) UnmarshalQueryParameter(str string) error {
if len(str) == 0 {
t.Time = time.Time{}
return nil
}
// Tolerate requests from older clients that used JSON serialization to build query params
if len(str) == 4 && str == "null" {
t.Time = time.Time{}
return nil
}
pt, err := time.Parse(time.RFC3339, str)
if err != nil {
return err
}
t.Time = pt.Local()
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (t Time) MarshalJSON() ([]byte, error) {
if t.IsZero() {
// Encode unset/nil objects as JSON's "null".
return []byte("null"), nil
}
return json.Marshal(t.UTC().Format(time.RFC3339))
}
// MarshalQueryParameter converts to a URL query parameter value
func (t Time) MarshalQueryParameter() (string, error) {
if t.IsZero() {
// Encode unset/nil objects as an empty string
return "", nil
}
return t.UTC().Format(time.RFC3339), nil
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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
http://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 k8sapi
// TypeMeta describes an individual object in an API response or request
// with strings representing the type of the object and its API schema version.
// Structures that are versioned or persisted should inline TypeMeta.
type TypeMeta struct {
// Kind is a string value representing the REST resource this object represents.
// Servers may infer this from the endpoint the client submits requests to.
// Cannot be updated.
// In CamelCase.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#types-kinds
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
// APIVersion defines the versioned schema of this representation of an object.
// Servers should convert recognized schemas to the latest internal value, and
// may reject unrecognized values.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#resources
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
// ListMeta describes metadata that synthetic resources must have, including lists and
// various status objects. A resource may have only one of {ObjectMeta, ListMeta}.
type ListMeta struct {
// SelfLink is a URL representing this object.
// Populated by the system.
// Read-only.
SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,1,opt,name=selfLink"`
// String that identifies the server's internal version of this object that
// can be used by clients to determine when objects have changed.
// Value must be treated as opaque by clients and passed unmodified back to the server.
// Populated by the system.
// Read-only.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#concurrency-control-and-consistency
ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,2,opt,name=resourceVersion"`
}

View File

@@ -0,0 +1,162 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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
http://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 k8sapi
// ObjectMeta is metadata that all persisted resources must have, which includes all objects
// users must create.
type ObjectMeta struct {
// Name must be unique within a namespace. Is required when creating resources, although
// some resources may allow a client to request the generation of an appropriate name
// automatically. Name is primarily intended for creation idempotence and configuration
// definition.
// Cannot be updated.
// More info: http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#names
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
// GenerateName is an optional prefix, used by the server, to generate a unique
// name ONLY IF the Name field has not been provided.
// If this field is used, the name returned to the client will be different
// than the name passed. This value will also be combined with a unique suffix.
// The provided value has the same validation rules as the Name field,
// and may be truncated by the length of the suffix required to make the value
// unique on the server.
//
// If this field is specified and the generated name exists, the server will
// NOT return a 409 - instead, it will either return 201 Created or 500 with Reason
// ServerTimeout indicating a unique name could not be found in the time allotted, and the client
// should retry (optionally after the time indicated in the Retry-After header).
//
// Applied only if Name is not specified.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#idempotency
GenerateName string `json:"generateName,omitempty" protobuf:"bytes,2,opt,name=generateName"`
// Namespace defines the space within each name must be unique. An empty namespace is
// equivalent to the "default" namespace, but "default" is the canonical representation.
// Not all objects are required to be scoped to a namespace - the value of this field for
// those objects will be empty.
//
// Must be a DNS_LABEL.
// Cannot be updated.
// More info: http://releases.k8s.io/release-1.3/docs/user-guide/namespaces.md
Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"`
// SelfLink is a URL representing this object.
// Populated by the system.
// Read-only.
SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,4,opt,name=selfLink"`
// UID is the unique in time and space value for this object. It is typically generated by
// the server on successful creation of a resource and is not allowed to change on PUT
// operations.
//
// Populated by the system.
// Read-only.
// More info: http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#uids
UID string `json:"uid,omitempty" protobuf:"bytes,5,opt,name=uid,casttype=k8s.io/kubernetes/pkg/types.UID"`
// An opaque value that represents the internal version of this object that can
// be used by clients to determine when objects have changed. May be used for optimistic
// concurrency, change detection, and the watch operation on a resource or set of resources.
// Clients must treat these values as opaque and passed unmodified back to the server.
// They may only be valid for a particular resource or set of resources.
//
// Populated by the system.
// Read-only.
// Value must be treated as opaque by clients and .
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#concurrency-control-and-consistency
ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,6,opt,name=resourceVersion"`
// A sequence number representing a specific generation of the desired state.
// Populated by the system. Read-only.
Generation int64 `json:"generation,omitempty" protobuf:"varint,7,opt,name=generation"`
// CreationTimestamp is a timestamp representing the server time when this object was
// created. It is not guaranteed to be set in happens-before order across separate operations.
// Clients may not set this value. It is represented in RFC3339 form and is in UTC.
//
// Populated by the system.
// Read-only.
// Null for lists.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#metadata
CreationTimestamp Time `json:"creationTimestamp,omitempty" protobuf:"bytes,8,opt,name=creationTimestamp"`
// DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This
// field is set by the server when a graceful deletion is requested by the user, and is not
// directly settable by a client. The resource will be deleted (no longer visible from
// resource lists, and not reachable by name) after the time in this field. Once set, this
// value may not be unset or be set further into the future, although it may be shortened
// or the resource may be deleted prior to this time. For example, a user may request that
// a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination
// signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet
// will send a hard termination signal to the container.
// If not set, graceful deletion of the object has not been requested.
//
// Populated by the system when a graceful deletion is requested.
// Read-only.
// More info: http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#metadata
DeletionTimestamp *Time `json:"deletionTimestamp,omitempty" protobuf:"bytes,9,opt,name=deletionTimestamp"`
// Number of seconds allowed for this object to gracefully terminate before
// it will be removed from the system. Only set when deletionTimestamp is also set.
// May only be shortened.
// Read-only.
DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty" protobuf:"varint,10,opt,name=deletionGracePeriodSeconds"`
// Map of string keys and values that can be used to organize and categorize
// (scope and select) objects. May match selectors of replication controllers
// and services.
// More info: http://releases.k8s.io/release-1.3/docs/user-guide/labels.md
// TODO: replace map[string]string with labels.LabelSet type
Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"`
// Annotations is an unstructured key value map stored with a resource that may be
// set by external tools to store and retrieve arbitrary metadata. They are not
// queryable and should be preserved when modifying objects.
// More info: http://releases.k8s.io/release-1.3/docs/user-guide/annotations.md
Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"`
// List of objects depended by this object. If ALL objects in the list have
// been deleted, this object will be garbage collected. If this object is managed by a controller,
// then an entry in this list will point to this controller, with the controller field set to true.
// There cannot be more than one managing controller.
OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" patchStrategy:"merge" patchMergeKey:"uid" protobuf:"bytes,13,rep,name=ownerReferences"`
// Must be empty before the object is deleted from the registry. Each entry
// is an identifier for the responsible component that will remove the entry
// from the list. If the deletionTimestamp of the object is non-nil, entries
// in this list can only be removed.
Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"`
}
// OwnerReference contains enough information to let you identify an owning
// object. Currently, an owning object must be in the same namespace, so there
// is no namespace field.
type OwnerReference struct {
// API version of the referent.
APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
// Kind of the referent.
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
// Name of the referent.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#names
Name string `json:"name" protobuf:"bytes,3,opt,name=name"`
// UID of the referent.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#uids
UID string `json:"uid" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/kubernetes/pkg/types.UID"`
// If true, this reference points to the managing controller.
Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
}

View File

@@ -0,0 +1,234 @@
package kubernetes
import (
"errors"
"fmt"
"os"
"path/filepath"
homedir "github.com/mitchellh/go-homedir"
"github.com/coreos/poke/storage"
"github.com/coreos/poke/storage/kubernetes/k8sapi"
)
const (
kindAuthCode = "AuthCode"
kindAuthRequest = "AuthRequest"
kindClient = "OAuth2Client"
kindRefreshToken = "RefreshToken"
kindKeys = "SigningKey"
)
const (
resourceAuthCode = "authcodes"
resourceAuthRequest = "authrequests"
resourceClient = "oauth2clients"
resourceRefreshToken = "refreshtokens"
resourceKeys = "signingkeies" // Kubernetes attempts to pluralize.
)
// Config values for the Kubernetes storage type.
type Config struct {
InCluster bool `yaml:"inCluster"`
KubeConfigPath string `yaml:"kubeConfigPath"`
}
// Open returns a storage using Kubernetes third party resource.
func (c *Config) Open() (storage.Storage, error) {
if c.InCluster && (c.KubeConfigPath != "") {
return nil, errors.New("cannot specify both 'inCluster' and 'kubeConfigPath'")
}
var (
cluster k8sapi.Cluster
user k8sapi.AuthInfo
namespace string
err error
)
if c.InCluster {
cluster, user, namespace, err = inClusterConfig()
} else {
kubeConfigPath := c.KubeConfigPath
if kubeConfigPath == "" {
kubeConfigPath = os.Getenv("KUBECONFIG")
}
if kubeConfigPath == "" {
p, err := homedir.Dir()
if err != nil {
return nil, fmt.Errorf("finding homedir: %v", err)
}
kubeConfigPath = filepath.Join(p, ".kube", "config")
}
cluster, user, namespace, err = loadKubeConfig(kubeConfigPath)
}
if err != nil {
return nil, err
}
return newClient(cluster, user, namespace)
}
func (cli *client) Close() error {
return nil
}
func (cli *client) CreateAuthRequest(a storage.AuthRequest) error {
return cli.post(resourceAuthRequest, cli.fromStorageAuthRequest(a))
}
func (cli *client) CreateClient(c storage.Client) error {
return cli.post(resourceClient, cli.fromStorageClient(c))
}
func (cli *client) CreateAuthCode(c storage.AuthCode) error {
return cli.post(resourceAuthCode, cli.fromStorageAuthCode(c))
}
func (cli *client) CreateRefresh(r storage.Refresh) error {
refresh := Refresh{
TypeMeta: k8sapi.TypeMeta{
Kind: kindRefreshToken,
APIVersion: cli.apiVersionForResource(resourceRefreshToken),
},
ObjectMeta: k8sapi.ObjectMeta{
Name: r.RefreshToken,
Namespace: cli.namespace,
},
ClientID: r.ClientID,
ConnectorID: r.ConnectorID,
Scopes: r.Scopes,
Nonce: r.Nonce,
Identity: fromStorageIdentity(r.Identity),
}
return cli.post(resourceRefreshToken, refresh)
}
func (cli *client) GetAuthRequest(id string) (storage.AuthRequest, error) {
var req AuthRequest
if err := cli.get(resourceAuthRequest, id, &req); err != nil {
return storage.AuthRequest{}, err
}
return toStorageAuthRequest(req), nil
}
func (cli *client) GetAuthCode(id string) (storage.AuthCode, error) {
var code AuthCode
if err := cli.get(resourceAuthCode, id, &code); err != nil {
return storage.AuthCode{}, err
}
return toStorageAuthCode(code), nil
}
func (cli *client) GetClient(id string) (storage.Client, error) {
var c Client
if err := cli.get(resourceClient, id, &c); err != nil {
return storage.Client{}, err
}
return toStorageClient(c), nil
}
func (cli *client) GetKeys() (storage.Keys, error) {
var keys Keys
if err := cli.get(resourceKeys, keysName, &keys); err != nil {
return storage.Keys{}, err
}
return toStorageKeys(keys), nil
}
func (cli *client) GetRefresh(id string) (storage.Refresh, error) {
var r Refresh
if err := cli.get(resourceRefreshToken, id, &r); err != nil {
return storage.Refresh{}, err
}
return storage.Refresh{
RefreshToken: r.ObjectMeta.Name,
ClientID: r.ClientID,
ConnectorID: r.ConnectorID,
Scopes: r.Scopes,
Nonce: r.Nonce,
Identity: toStorageIdentity(r.Identity),
}, nil
}
func (cli *client) ListClients() ([]storage.Client, error) {
return nil, errors.New("not implemented")
}
func (cli *client) ListRefreshTokens() ([]storage.Refresh, error) {
return nil, errors.New("not implemented")
}
func (cli *client) DeleteAuthRequest(id string) error {
return cli.delete(resourceAuthRequest, id)
}
func (cli *client) DeleteAuthCode(code string) error {
return cli.delete(resourceAuthCode, code)
}
func (cli *client) DeleteClient(id string) error {
return cli.delete(resourceClient, id)
}
func (cli *client) DeleteRefresh(id string) error {
return cli.delete(resourceRefreshToken, id)
}
func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error {
var c Client
if err := cli.get(resourceClient, id, &c); err != nil {
return err
}
updated, err := updater(toStorageClient(c))
if err != nil {
return err
}
newClient := cli.fromStorageClient(updated)
newClient.ObjectMeta = c.ObjectMeta
return cli.put(resourceClient, id, newClient)
}
func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error {
firstUpdate := false
var keys Keys
if err := cli.get(resourceKeys, keysName, &keys); err != nil {
if err != storage.ErrNotFound {
return err
}
firstUpdate = true
}
var oldKeys storage.Keys
if !firstUpdate {
oldKeys = toStorageKeys(keys)
}
updated, err := updater(oldKeys)
if err != nil {
return err
}
newKeys := cli.fromStorageKeys(updated)
if firstUpdate {
return cli.post(resourceKeys, newKeys)
}
newKeys.ObjectMeta = keys.ObjectMeta
return cli.put(resourceKeys, keysName, newKeys)
}
func (cli *client) UpdateAuthRequest(id string, updater func(a storage.AuthRequest) (storage.AuthRequest, error)) error {
var req AuthRequest
err := cli.get(resourceAuthRequest, id, &req)
if err != nil {
return err
}
updated, err := updater(toStorageAuthRequest(req))
if err != nil {
return err
}
newReq := cli.fromStorageAuthRequest(updated)
newReq.ObjectMeta = req.ObjectMeta
return cli.put(resourceAuthRequest, id, newReq)
}

View File

@@ -0,0 +1,78 @@
package kubernetes
import (
"os"
"testing"
"github.com/coreos/poke/storage"
"github.com/coreos/poke/storage/storagetest"
)
func TestLoadClient(t *testing.T) {
loadClient(t)
}
func loadClient(t *testing.T) storage.Storage {
if os.Getenv("KUBECONFIG") == "" {
t.Skip()
}
var config Config
s, err := config.Open()
if err != nil {
t.Fatal(err)
}
return s
}
func TestURLFor(t *testing.T) {
tests := []struct {
apiVersion, namespace, resource, name string
baseURL string
want string
}{
{
"v1", "default", "pods", "a",
"https://k8s.example.com",
"https://k8s.example.com/api/v1/namespaces/default/pods/a",
},
{
"foo/v1", "default", "bar", "a",
"https://k8s.example.com",
"https://k8s.example.com/apis/foo/v1/namespaces/default/bar/a",
},
{
"foo/v1", "default", "bar", "a",
"https://k8s.example.com/",
"https://k8s.example.com/apis/foo/v1/namespaces/default/bar/a",
},
{
"foo/v1", "default", "bar", "a",
"https://k8s.example.com/",
"https://k8s.example.com/apis/foo/v1/namespaces/default/bar/a",
},
{
// no namespace
"foo/v1", "", "bar", "a",
"https://k8s.example.com",
"https://k8s.example.com/apis/foo/v1/bar/a",
},
}
for _, test := range tests {
c := &client{baseURL: test.baseURL, prependResourceNameToAPIGroup: false}
got := c.urlFor(test.apiVersion, test.namespace, test.resource, test.name)
if got != test.want {
t.Errorf("(&client{baseURL:%q}).urlFor(%q, %q, %q, %q): expected %q got %q",
test.baseURL,
test.apiVersion, test.namespace, test.resource, test.name,
test.want, got,
)
}
}
}
func TestStorage(t *testing.T) {
client := loadClient(t)
storagetest.RunTestSuite(t, client)
}

309
storage/kubernetes/types.go Normal file
View File

@@ -0,0 +1,309 @@
package kubernetes
import (
"time"
jose "gopkg.in/square/go-jose.v2"
"github.com/coreos/poke/storage"
"github.com/coreos/poke/storage/kubernetes/k8sapi"
)
// There will only ever be a single keys resource. Maintain this by setting a
// common name.
const keysName = "openid-connect-keys"
// Client is a mirrored struct from storage with JSON struct tags and
// Kubernetes type metadata.
type Client struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
Secret string `json:"secret,omitempty"`
RedirectURIs []string `json:"redirectURIs,omitempty"`
TrustedPeers []string `json:"trustedPeers,omitempty"`
Public bool `json:"public"`
Name string `json:"name,omitempty"`
LogoURL string `json:"logoURL,omitempty"`
}
// ClientList is a list of Clients.
type ClientList struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ListMeta `json:"metadata,omitempty"`
Clients []Client `json:"items"`
}
func (cli *client) fromStorageClient(c storage.Client) Client {
return Client{
TypeMeta: k8sapi.TypeMeta{
Kind: kindClient,
APIVersion: cli.apiVersionForResource(resourceClient),
},
ObjectMeta: k8sapi.ObjectMeta{
Name: c.ID,
Namespace: cli.namespace,
},
Secret: c.Secret,
RedirectURIs: c.RedirectURIs,
TrustedPeers: c.TrustedPeers,
Public: c.Public,
Name: c.Name,
LogoURL: c.LogoURL,
}
}
func toStorageClient(c Client) storage.Client {
return storage.Client{
ID: c.ObjectMeta.Name,
Secret: c.Secret,
RedirectURIs: c.RedirectURIs,
TrustedPeers: c.TrustedPeers,
Public: c.Public,
Name: c.Name,
LogoURL: c.LogoURL,
}
}
// Identity is a mirrored struct from storage with JSON struct tags.
type Identity struct {
UserID string `json:"userID"`
Username string `json:"username"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
Groups []string `json:"groups,omitempty"`
ConnectorData []byte `json:"connectorData,omitempty"`
}
func fromStorageIdentity(i storage.Identity) Identity {
return Identity{
UserID: i.UserID,
Username: i.Username,
Email: i.Email,
EmailVerified: i.EmailVerified,
Groups: i.Groups,
ConnectorData: i.ConnectorData,
}
}
func toStorageIdentity(i Identity) storage.Identity {
return storage.Identity{
UserID: i.UserID,
Username: i.Username,
Email: i.Email,
EmailVerified: i.EmailVerified,
Groups: i.Groups,
ConnectorData: i.ConnectorData,
}
}
// AuthRequest is a mirrored struct from storage with JSON struct tags and
// Kubernetes type metadata.
type AuthRequest struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
ClientID string `json:"clientID"`
ResponseTypes []string `json:"responseTypes,omitempty"`
Scopes []string `json:"scopes,omitempty"`
RedirectURI string `json:"redirectURI"`
Nonce string `json:"nonce,omitempty"`
State string `json:"state,omitempty"`
// The client has indicated that the end user must be shown an approval prompt
// on all requests. The server cannot cache their initial action for subsequent
// attempts.
ForceApprovalPrompt bool `json:"forceApprovalPrompt,omitempty"`
// The identity of the end user. Generally nil until the user authenticates
// with a backend.
Identity *Identity `json:"identity,omitempty"`
// The connector used to login the user. Set when the user authenticates.
ConnectorID string `json:"connectorID,omitempty"`
Expiry time.Time `json:"expiry"`
}
// AuthRequestList is a list of AuthRequests.
type AuthRequestList struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ListMeta `json:"metadata,omitempty"`
AuthRequests []AuthRequest `json:"items"`
}
func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
a := storage.AuthRequest{
ID: req.ObjectMeta.Name,
ClientID: req.ClientID,
ResponseTypes: req.ResponseTypes,
Scopes: req.Scopes,
RedirectURI: req.RedirectURI,
Nonce: req.Nonce,
State: req.State,
ForceApprovalPrompt: req.ForceApprovalPrompt,
ConnectorID: req.ConnectorID,
Expiry: req.Expiry,
}
if req.Identity != nil {
i := toStorageIdentity(*req.Identity)
a.Identity = &i
}
return a
}
func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
req := AuthRequest{
TypeMeta: k8sapi.TypeMeta{
Kind: kindAuthRequest,
APIVersion: cli.apiVersionForResource(resourceAuthRequest),
},
ObjectMeta: k8sapi.ObjectMeta{
Name: a.ID,
Namespace: cli.namespace,
},
ClientID: a.ClientID,
ResponseTypes: a.ResponseTypes,
Scopes: a.Scopes,
RedirectURI: a.RedirectURI,
Nonce: a.Nonce,
State: a.State,
ForceApprovalPrompt: a.ForceApprovalPrompt,
ConnectorID: a.ConnectorID,
Expiry: a.Expiry,
}
if a.Identity != nil {
i := fromStorageIdentity(*a.Identity)
req.Identity = &i
}
return req
}
// AuthCode is a mirrored struct from storage with JSON struct tags and
// Kubernetes type metadata.
type AuthCode struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
ClientID string `json:"clientID"`
Scopes []string `json:"scopes,omitempty"`
RedirectURI string `json:"redirectURI"`
Nonce string `json:"nonce,omitempty"`
State string `json:"state,omitempty"`
Identity Identity `json:"identity,omitempty"`
ConnectorID string `json:"connectorID,omitempty"`
Expiry time.Time `json:"expiry"`
}
// AuthCodeList is a list of AuthCodes.
type AuthCodeList struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ListMeta `json:"metadata,omitempty"`
AuthCodes []AuthCode `json:"items"`
}
func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode {
return AuthCode{
TypeMeta: k8sapi.TypeMeta{
Kind: kindAuthCode,
APIVersion: cli.apiVersionForResource(resourceAuthCode),
},
ObjectMeta: k8sapi.ObjectMeta{
Name: a.ID,
Namespace: cli.namespace,
},
ClientID: a.ClientID,
RedirectURI: a.RedirectURI,
ConnectorID: a.ConnectorID,
Nonce: a.Nonce,
Scopes: a.Scopes,
Identity: fromStorageIdentity(a.Identity),
Expiry: a.Expiry,
}
}
func toStorageAuthCode(a AuthCode) storage.AuthCode {
return storage.AuthCode{
ID: a.ObjectMeta.Name,
ClientID: a.ClientID,
RedirectURI: a.RedirectURI,
ConnectorID: a.ConnectorID,
Nonce: a.Nonce,
Scopes: a.Scopes,
Identity: toStorageIdentity(a.Identity),
Expiry: a.Expiry,
}
}
// Refresh is a mirrored struct from storage with JSON struct tags and
// Kubernetes type metadata.
type Refresh struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
ClientID string `json:"clientID"`
Scopes []string `json:"scopes,omitempty"`
Nonce string `json:"nonce,omitempty"`
Identity Identity `json:"identity,omitempty"`
ConnectorID string `json:"connectorID,omitempty"`
}
// RefreshList is a list of refresh tokens.
type RefreshList struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ListMeta `json:"metadata,omitempty"`
RefreshTokens []Refresh `json:"items"`
}
// Keys is a mirrored struct from storage with JSON struct tags and Kubernetes
// type metadata.
type Keys struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
// Key for creating and verifying signatures. These may be nil.
SigningKey *jose.JSONWebKey `json:"signingKey,omitempty"`
SigningKeyPub *jose.JSONWebKey `json:"signingKeyPub,omitempty"`
// Old signing keys which have been rotated but can still be used to validate
// existing signatures.
VerificationKeys []storage.VerificationKey `json:"verificationKeys,omitempty"`
// The next time the signing key will rotate.
//
// For caching purposes, implementations MUST NOT update keys before this time.
NextRotation time.Time `json:"nextRotation"`
}
func (cli *client) fromStorageKeys(keys storage.Keys) Keys {
return Keys{
TypeMeta: k8sapi.TypeMeta{
Kind: kindKeys,
APIVersion: cli.apiVersionForResource(resourceKeys),
},
ObjectMeta: k8sapi.ObjectMeta{
Name: keysName,
Namespace: cli.namespace,
},
SigningKey: keys.SigningKey,
SigningKeyPub: keys.SigningKeyPub,
VerificationKeys: keys.VerificationKeys,
NextRotation: keys.NextRotation,
}
}
func toStorageKeys(keys Keys) storage.Keys {
return storage.Keys{
SigningKey: keys.SigningKey,
SigningKeyPub: keys.SigningKeyPub,
VerificationKeys: keys.VerificationKeys,
NextRotation: keys.NextRotation,
}
}