Skip to content
This repository was archived by the owner on Mar 22, 2018. It is now read-only.

Commit d77f6e6

Browse files
author
Kubernetes Submit Queue
authored
Merge pull request #39587 from zhouhaibing089/openstack-auth-provider
Automatic merge from submit-queue (batch tested with PRs 50087, 39587, 50042, 50241, 49914) plugin/pkg/client/auth: add openstack auth provider This is an implementation of auth provider for OpenStack world, just like python-openstackclient, we read the environment variables of a list `OS_*`, and client will cache a token to interact with each components, we can do the same here, the client side can cache a token locally at the first time, and rotate automatically when it expires. This requires an implementation of token authenticator at server side, refer: 1. [made by me] kubernetes/kubernetes#25536, I can carry this on when it is fine to go. 2. [made by @kfox1111] kubernetes/kubernetes#25391 The reason why I want to add this is due to the `client-side` nature, it will be confusing to implement it downstream, we would like to add this support here, and customers can get `kubectl` like they usually do(`brew install kubernetes-cli`), and it will just work. When this is done, we can deprecate the password keystone authenticator as the following reasons: 1. as mentioned at some other places, the `domain` is another parameters which should be provided. 2. in case the user supplies `apikey` and `secrets`, we might want to fill the `UserInfo` with the real name which is not implemented for now. cc @erictune @liggitt ``` add openstack auth provider ```
2 parents fc99c7a + 5f5e765 commit d77f6e6

3 files changed

Lines changed: 317 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
licenses(["notice"])
4+
5+
load(
6+
"@io_bazel_rules_go//go:def.bzl",
7+
"go_library",
8+
"go_test",
9+
)
10+
11+
go_test(
12+
name = "go_default_test",
13+
srcs = ["openstack_test.go"],
14+
library = ":go_default_library",
15+
tags = ["automanaged"],
16+
)
17+
18+
go_library(
19+
name = "go_default_library",
20+
srcs = ["openstack.go"],
21+
tags = ["automanaged"],
22+
deps = [
23+
"//vendor/github.com/golang/glog:go_default_library",
24+
"//vendor/github.com/gophercloud/gophercloud/openstack:go_default_library",
25+
"//vendor/k8s.io/client-go/rest:go_default_library",
26+
],
27+
)
28+
29+
filegroup(
30+
name = "package-srcs",
31+
srcs = glob(["**"]),
32+
tags = ["automanaged"],
33+
visibility = ["//visibility:private"],
34+
)
35+
36+
filegroup(
37+
name = "all-srcs",
38+
srcs = [":package-srcs"],
39+
tags = ["automanaged"],
40+
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package openstack
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"sync"
23+
"time"
24+
25+
"github.com/golang/glog"
26+
"github.com/gophercloud/gophercloud/openstack"
27+
28+
restclient "k8s.io/client-go/rest"
29+
)
30+
31+
func init() {
32+
if err := restclient.RegisterAuthProviderPlugin("openstack", newOpenstackAuthProvider); err != nil {
33+
glog.Fatalf("Failed to register openstack auth plugin: %s", err)
34+
}
35+
}
36+
37+
// DefaultTTLDuration is the time before a token gets expired.
38+
const DefaultTTLDuration = 10 * time.Minute
39+
40+
// openstackAuthProvider is an authprovider for openstack. this provider reads
41+
// the environment variables to determine the client identity, and generates a
42+
// token which will be inserted into the request header later.
43+
type openstackAuthProvider struct {
44+
ttl time.Duration
45+
46+
tokenGetter TokenGetter
47+
}
48+
49+
// TokenGetter returns a bearer token that can be inserted into request.
50+
type TokenGetter interface {
51+
Token() (string, error)
52+
}
53+
54+
type tokenGetter struct{}
55+
56+
// Token creates a token by authenticate with keystone.
57+
func (*tokenGetter) Token() (string, error) {
58+
options, err := openstack.AuthOptionsFromEnv()
59+
if err != nil {
60+
return "", fmt.Errorf("failed to read openstack env vars: %s", err)
61+
}
62+
client, err := openstack.AuthenticatedClient(options)
63+
if err != nil {
64+
return "", fmt.Errorf("authentication failed: %s", err)
65+
}
66+
return client.TokenID, nil
67+
}
68+
69+
// cachedGetter caches a token until it gets expired, after the expiration, it will
70+
// generate another token and cache it.
71+
type cachedGetter struct {
72+
mutex sync.Mutex
73+
tokenGetter TokenGetter
74+
75+
token string
76+
born time.Time
77+
ttl time.Duration
78+
}
79+
80+
// Token returns the current available token, create a new one if expired.
81+
func (c *cachedGetter) Token() (string, error) {
82+
c.mutex.Lock()
83+
defer c.mutex.Unlock()
84+
85+
var err error
86+
// no token or exceeds the TTL
87+
if c.token == "" || time.Now().Sub(c.born) > c.ttl {
88+
c.token, err = c.tokenGetter.Token()
89+
if err != nil {
90+
return "", fmt.Errorf("failed to get token: %s", err)
91+
}
92+
c.born = time.Now()
93+
}
94+
return c.token, nil
95+
}
96+
97+
// tokenRoundTripper implements the RoundTripper interface: adding the bearer token
98+
// into the request header.
99+
type tokenRoundTripper struct {
100+
http.RoundTripper
101+
102+
tokenGetter TokenGetter
103+
}
104+
105+
// RoundTrip adds the bearer token into the request.
106+
func (t *tokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
107+
// if the authorization header already present, use it.
108+
if req.Header.Get("Authorization") != "" {
109+
return t.RoundTripper.RoundTrip(req)
110+
}
111+
112+
token, err := t.tokenGetter.Token()
113+
if err == nil {
114+
req.Header.Set("Authorization", "Bearer "+token)
115+
} else {
116+
glog.V(4).Infof("failed to get token: %s", err)
117+
}
118+
119+
return t.RoundTripper.RoundTrip(req)
120+
}
121+
122+
// newOpenstackAuthProvider creates an auth provider which works with openstack
123+
// environment.
124+
func newOpenstackAuthProvider(clusterAddress string, config map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
125+
var ttlDuration time.Duration
126+
var err error
127+
128+
ttl, found := config["ttl"]
129+
if !found {
130+
ttlDuration = DefaultTTLDuration
131+
// persist to config
132+
config["ttl"] = ttlDuration.String()
133+
if err = persister.Persist(config); err != nil {
134+
return nil, fmt.Errorf("failed to persist config: %s", err)
135+
}
136+
} else {
137+
ttlDuration, err = time.ParseDuration(ttl)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to parse ttl config: %s", err)
140+
}
141+
}
142+
143+
// TODO: read/persist client configuration(OS_XXX env vars) in config
144+
145+
return &openstackAuthProvider{
146+
ttl: ttlDuration,
147+
tokenGetter: &tokenGetter{},
148+
}, nil
149+
}
150+
151+
func (oap *openstackAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
152+
return &tokenRoundTripper{
153+
RoundTripper: rt,
154+
tokenGetter: &cachedGetter{
155+
tokenGetter: oap.tokenGetter,
156+
ttl: oap.ttl,
157+
},
158+
}
159+
}
160+
161+
func (oap *openstackAuthProvider) Login() error { return nil }
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package openstack
18+
19+
import (
20+
"math/rand"
21+
"net/http"
22+
"testing"
23+
"time"
24+
)
25+
26+
// testTokenGetter is a simple random token getter.
27+
type testTokenGetter struct{}
28+
29+
const LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
30+
31+
func RandStringBytes(n int) string {
32+
b := make([]byte, n)
33+
for i := range b {
34+
b[i] = LetterBytes[rand.Intn(len(LetterBytes))]
35+
}
36+
return string(b)
37+
}
38+
39+
func (*testTokenGetter) Token() (string, error) {
40+
return RandStringBytes(32), nil
41+
}
42+
43+
// testRoundTripper is mocked roundtripper which responds with unauthorized when
44+
// there is no authorization header, otherwise returns status ok.
45+
type testRoundTripper struct{}
46+
47+
func (trt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
48+
authHeader := req.Header.Get("Authorization")
49+
if authHeader == "" || authHeader == "Bearer " {
50+
return &http.Response{
51+
StatusCode: http.StatusUnauthorized,
52+
}, nil
53+
}
54+
return &http.Response{StatusCode: http.StatusOK}, nil
55+
}
56+
57+
func TestOpenstackAuthProvider(t *testing.T) {
58+
trt := &tokenRoundTripper{
59+
RoundTripper: &testRoundTripper{},
60+
}
61+
62+
tests := []struct {
63+
name string
64+
ttl time.Duration
65+
interval time.Duration
66+
same bool
67+
}{
68+
{
69+
name: "normal",
70+
ttl: 2 * time.Second,
71+
interval: 1 * time.Second,
72+
same: true,
73+
},
74+
{
75+
name: "expire",
76+
ttl: 1 * time.Second,
77+
interval: 2 * time.Second,
78+
same: false,
79+
},
80+
}
81+
82+
for _, test := range tests {
83+
trt.tokenGetter = &cachedGetter{
84+
tokenGetter: &testTokenGetter{},
85+
ttl: test.ttl,
86+
}
87+
88+
req, err := http.NewRequest(http.MethodPost, "https://test-api-server.com", nil)
89+
if err != nil {
90+
t.Errorf("failed to new request: %s", err)
91+
}
92+
trt.RoundTrip(req)
93+
header := req.Header.Get("Authorization")
94+
if header == "" {
95+
t.Errorf("expect to see token in header, but is absent")
96+
}
97+
98+
time.Sleep(test.interval)
99+
100+
req, err = http.NewRequest(http.MethodPost, "https://test-api-server.com", nil)
101+
if err != nil {
102+
t.Errorf("failed to new request: %s", err)
103+
}
104+
trt.RoundTrip(req)
105+
newHeader := req.Header.Get("Authorization")
106+
if newHeader == "" {
107+
t.Errorf("expect to see token in header, but is absent")
108+
}
109+
110+
same := newHeader == header
111+
if same != test.same {
112+
t.Errorf("expect to get %t when compare header, but saw %t", test.same, same)
113+
}
114+
}
115+
116+
}

0 commit comments

Comments
 (0)