From 691476b4779526098dd10879bbbedfb4262ec967 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Thu, 13 Oct 2016 16:50:20 -0700 Subject: [PATCH 1/2] storage/kubernetes: manage third party resources and drop support for 1.3 --- storage/kubernetes/client.go | 60 +++++++++++++------------- storage/kubernetes/storage.go | 54 ++++++++++++++++++++++-- storage/kubernetes/storage_test.go | 2 +- storage/kubernetes/types.go | 68 +++++++++++++++++++++++++++--- 4 files changed, 146 insertions(+), 38 deletions(-) diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index 2c0910a7..bfdc9151 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -20,6 +20,7 @@ import ( "time" "github.com/gtank/cryptopasta" + "golang.org/x/net/context" yaml "gopkg.in/yaml.v2" "github.com/coreos/dex/storage" @@ -27,27 +28,17 @@ import ( ) type client struct { - client *http.Client - baseURL string - namespace string + client *http.Client + baseURL string + namespace string + + // API version of the oidc resources. For example "oidc.coreos.com". This is + // currently not configurable, but could be in the future. 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 + // This is called once the client's Close method is called to signal goroutines, + // such as the one creating third party resources, to stop. + cancel context.CancelFunc } func (c *client) urlFor(apiVersion, namespace, resource, name string) string { @@ -56,10 +47,6 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string { basePath = "api/" } - if c.prependResourceNameToAPIGroup && apiVersion != "" && resource != "" { - apiVersion = resource + "." + apiVersion - } - var p string if namespace != "" { p = path.Join(basePath, apiVersion, "namespaces", namespace, resource, name) @@ -72,15 +59,28 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string { return c.baseURL + "/" + p } +// Define an error interface so we can get at the underlying status code if it's +// absolutely necessary. For instance when we need to see if an error indicates +// a resource already exists. +type httpError interface { + StatusCode() int +} + +var _ httpError = (*httpErr)(nil) + type httpErr struct { method string url string - status string + status int body []byte } +func (e *httpErr) StatusCode() int { + return e.status +} + 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)) + return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, http.StatusText(e.status), bytes.TrimSpace(e.body)) } func checkHTTPErr(r *http.Response, validStatusCodes ...int) error { @@ -100,7 +100,7 @@ func checkHTTPErr(r *http.Response, validStatusCodes ...int) error { method = r.Request.Method url = r.Request.URL.String() } - err = &httpErr{method, url, r.Status, body} + err = &httpErr{method, url, r.StatusCode, body} log.Printf("%s", err) if r.StatusCode == http.StatusNotFound { @@ -134,12 +134,16 @@ func (c *client) list(resource string, v interface{}) error { } func (c *client) post(resource string, v interface{}) error { + return c.postResource(c.apiVersion, c.namespace, resource, v) +} + +func (c *client) postResource(apiVersion, namespace, 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, "") + url := c.urlFor(apiVersion, namespace, resource, "") resp, err := c.client.Post(url, "application/json", bytes.NewReader(body)) if err != nil { return err @@ -277,8 +281,6 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) ( baseURL: cluster.Server, namespace: namespace, apiVersion: "oidc.coreos.com/v1", - now: time.Now, - prependResourceNameToAPIGroup: true, }, nil } diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go index 178a90db..7a7fb2b4 100644 --- a/storage/kubernetes/storage.go +++ b/storage/kubernetes/storage.go @@ -4,11 +4,13 @@ import ( "errors" "fmt" "log" + "net/http" "os" "path/filepath" "time" homedir "github.com/mitchellh/go-homedir" + "golang.org/x/net/context" "github.com/coreos/dex/storage" "github.com/coreos/dex/storage/kubernetes/k8sapi" @@ -45,7 +47,6 @@ func (c *Config) Open() (storage.Storage, error) { if err != nil { return nil, err } - return cli, nil } @@ -81,10 +82,57 @@ func (c *Config) open() (*client, error) { return nil, err } - return newClient(cluster, user, namespace) + cli, err := newClient(cluster, user, namespace) + if err != nil { + return nil, fmt.Errorf("create client: %v", err) + } + + // Don't try to synchronize this because creating third party resources is not + // a synchronous event. Even after the API server returns a 200, it can still + // take several seconds for them to actually appear. + ctx, cancel := context.WithCancel(context.Background()) + go func() { + for { + if err := cli.createThirdPartyResources(); err != nil { + log.Printf("failed creating third party resources: %v", err) + } else { + return + } + + select { + case <-ctx.Done(): + return + case <-time.After(30 * time.Second): + } + } + }() + + // If the client is closed, stop trying to create third party resources. + cli.cancel = cancel + return cli, nil +} + +func (cli *client) createThirdPartyResources() error { + for _, r := range thirdPartyResources { + err := cli.postResource("extensions/v1beta1", "", "thirdpartyresources", r) + if err != nil { + if e, ok := err.(httpError); ok { + if e.StatusCode() == http.StatusConflict { + log.Printf("third party resource already created %q", r.ObjectMeta.Name) + continue + } + } + return err + } + log.Printf("create third party resource %q", r.ObjectMeta.Name) + } + return nil } func (cli *client) Close() error { + if cli.cancel != nil { + cli.cancel() + } return nil } @@ -108,7 +156,7 @@ func (cli *client) CreateRefresh(r storage.RefreshToken) error { refresh := RefreshToken{ TypeMeta: k8sapi.TypeMeta{ Kind: kindRefreshToken, - APIVersion: cli.apiVersionForResource(resourceRefreshToken), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: r.RefreshToken, diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go index 043a1e9f..0f347730 100644 --- a/storage/kubernetes/storage_test.go +++ b/storage/kubernetes/storage_test.go @@ -60,7 +60,7 @@ func TestURLFor(t *testing.T) { } for _, test := range tests { - c := &client{baseURL: test.baseURL, prependResourceNameToAPIGroup: false} + c := &client{baseURL: test.baseURL} 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", diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 3c914e84..8c8f9d6e 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -11,6 +11,64 @@ import ( "github.com/coreos/dex/storage/kubernetes/k8sapi" ) +var tprMeta = k8sapi.TypeMeta{ + APIVersion: "extensions/v1beta1", + Kind: "ThirdPartyResource", +} + +// The set of third party resources required by the storage. These are managed by +// the storage so it can migrate itself by creating new resources. +var thirdPartyResources = []k8sapi.ThirdPartyResource{ + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "auth-code.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "A code which can be claimed for an access token.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "auth-request.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "A request for an end user to authorize a client.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "o-auth2-client.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "An OpenID Connect client.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "signing-key.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "Keys used to sign and verify OpenID Connect tokens.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "refresh-token.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "Refresh tokens for clients to continuously act on behalf of an end user.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, + { + ObjectMeta: k8sapi.ObjectMeta{ + Name: "password.oidc.coreos.com", + }, + TypeMeta: tprMeta, + Description: "Passwords managed by the OIDC server.", + Versions: []k8sapi.APIVersion{{Name: "v1"}}, + }, +} + // There will only ever be a single keys resource. Maintain this by setting a // common name. const keysName = "openid-connect-keys" @@ -45,7 +103,7 @@ func (cli *client) fromStorageClient(c storage.Client) Client { return Client{ TypeMeta: k8sapi.TypeMeta{ Kind: kindClient, - APIVersion: cli.apiVersionForResource(resourceClient), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: c.ID, @@ -162,7 +220,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest { req := AuthRequest{ TypeMeta: k8sapi.TypeMeta{ Kind: kindAuthRequest, - APIVersion: cli.apiVersionForResource(resourceAuthRequest), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: a.ID, @@ -216,7 +274,7 @@ func (cli *client) fromStoragePassword(p storage.Password) Password { return Password{ TypeMeta: k8sapi.TypeMeta{ Kind: kindPassword, - APIVersion: cli.apiVersionForResource(resourcePassword), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: emailToID(email), @@ -270,7 +328,7 @@ func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode { return AuthCode{ TypeMeta: k8sapi.TypeMeta{ Kind: kindAuthCode, - APIVersion: cli.apiVersionForResource(resourceAuthCode), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: a.ID, @@ -346,7 +404,7 @@ func (cli *client) fromStorageKeys(keys storage.Keys) Keys { return Keys{ TypeMeta: k8sapi.TypeMeta{ Kind: kindKeys, - APIVersion: cli.apiVersionForResource(resourceKeys), + APIVersion: cli.apiVersion, }, ObjectMeta: k8sapi.ObjectMeta{ Name: keysName, From b7c6eea3413a2c01a096825cea4ee9c657020944 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Thu, 13 Oct 2016 16:50:54 -0700 Subject: [PATCH 2/2] examples/k8s: update documentation --- examples/k8s/README.md | 1 - examples/k8s/thirdpartyresources.yaml | 57 --------------------------- 2 files changed, 58 deletions(-) delete mode 100644 examples/k8s/thirdpartyresources.yaml diff --git a/examples/k8s/README.md b/examples/k8s/README.md index 5162590d..1b195199 100644 --- a/examples/k8s/README.md +++ b/examples/k8s/README.md @@ -28,7 +28,6 @@ ConfigMap for dex to use. These run dex as a deployment with configuration and storage, allowing it to get started. ``` -kubectl create -f thirdpartyresources.yaml kubectl create configmap dex-config --from-file=config.yaml=config-k8s.yaml kubectl create -f deployment.yaml ``` diff --git a/examples/k8s/thirdpartyresources.yaml b/examples/k8s/thirdpartyresources.yaml deleted file mode 100644 index 40f03027..00000000 --- a/examples/k8s/thirdpartyresources.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# NOTE: Because of a bug in third party resources, each resource must be in it's -# own API Group. -# -# See fix at https://github.com/kubernetes/kubernetes/pull/28414 - -metadata: - name: auth-code.authcodes.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "A code which can be claimed for an access token." -versions: -- name: v1 ---- - -metadata: - name: auth-request.authrequests.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "A request for an end user to authorize a client." -versions: -- name: v1 ---- - -metadata: - name: o-auth2-client.oauth2clients.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "An OpenID Connect client." -versions: -- name: v1 ---- - -metadata: - name: signing-key.signingkeies.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "Keys used to sign and verify OpenID Connect tokens." -versions: -- name: v1 ---- - -metadata: - name: refresh-token.refreshtokens.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "Refresh tokens for clients to continuously act on behalf of an end user." -versions: -- name: v1 ---- - -metadata: - name: password.passwords.oidc.coreos.com -apiVersion: extensions/v1beta1 -kind: ThirdPartyResource -description: "Passwords managed by the OIDC server." -versions: -- name: v1