initial commit
This commit is contained in:
363
storage/kubernetes/client.go
Normal file
363
storage/kubernetes/client.go
Normal 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
|
||||
}
|
2
storage/kubernetes/doc.go
Normal file
2
storage/kubernetes/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package kubernetes provides a storage interface using Kubernetes third party APIs.
|
||||
package kubernetes
|
29
storage/kubernetes/garbage_collection.go
Normal file
29
storage/kubernetes/garbage_collection.go
Normal 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
|
||||
}
|
140
storage/kubernetes/k8sapi/client.go
Normal file
140
storage/kubernetes/k8sapi/client.go
Normal 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"`
|
||||
}
|
2
storage/kubernetes/k8sapi/doc.go
Normal file
2
storage/kubernetes/k8sapi/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package k8sapi holds vendored Kubernetes types.
|
||||
package k8sapi
|
49
storage/kubernetes/k8sapi/extensions.go
Normal file
49
storage/kubernetes/k8sapi/extensions.go
Normal 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"`
|
||||
}
|
138
storage/kubernetes/k8sapi/time.go
Normal file
138
storage/kubernetes/k8sapi/time.go
Normal 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
|
||||
}
|
52
storage/kubernetes/k8sapi/unversioned.go
Normal file
52
storage/kubernetes/k8sapi/unversioned.go
Normal 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"`
|
||||
}
|
162
storage/kubernetes/k8sapi/v1.go
Normal file
162
storage/kubernetes/k8sapi/v1.go
Normal 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"`
|
||||
}
|
234
storage/kubernetes/storage.go
Normal file
234
storage/kubernetes/storage.go
Normal 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)
|
||||
}
|
78
storage/kubernetes/storage_test.go
Normal file
78
storage/kubernetes/storage_test.go
Normal 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
309
storage/kubernetes/types.go
Normal 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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user