Skip to content

Commit 2b457e8

Browse files
authored
Combination of 2 commits which were removed from master (#3628)
plus a fix for the issue which caused removal Refactor tag manager rest client (#3412) - Adding back in after removal Force re-login of rest session in case of expired session 5041208 Implement session manager client on CSI (#3419) * Implement session manager client on CSI * Fix broken unittest 0ec9586 Add back early return 7aaffd0
1 parent dd7c806 commit 2b457e8

File tree

18 files changed

+657
-120
lines changed

18 files changed

+657
-120
lines changed

docs/book/vc_shared_sessions.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# vSphere Shared Session capability
2+
3+
One problem that can be found when provisioning a large amount of clusters using
4+
vSphere CSI is vCenter session exhaustion. This happens because every
5+
workload cluster needs to request a new session to vSphere to do proper reconciliation.
6+
7+
vSphere 8.0U3 and up uses a new approach of session management, that allows the
8+
creation and sharing of the sessions among different clusters.
9+
10+
A cluster admin can implement a rest API that, once called, requests a new vCenter
11+
session and shares with CSI. This session will not count on the total generated
12+
sessions of vSphere, and instead will be a child derived session.
13+
14+
This configuration can be applied on vSphere CSI with the usage of
15+
the following CSI configuration:
16+
17+
```shell
18+
[Global]
19+
ca-file = "/etc/ssl/certs/trusted-certificates.crt"
20+
[VirtualCenter "your-vcenter-host"]
21+
datacenters = "datacenter1"
22+
vc-session-manager-url = "https://some-session-manager/session"
23+
vc-session-manager-token = "a-secret-token"
24+
```
25+
26+
The configuration above will make CSI call the shared session rest API and use the
27+
provided token to authenticate against vSphere, instead of using a username/password.
28+
29+
The parameter provider at `vc-session-manager-token` is sent as a `Authorization: Bearer` token
30+
to the session manager, and in case this directive is not configured CSI will send the
31+
Pod Service Account token instead.
32+
33+
Below is an example implementation of a shared session manager rest API. Starting the
34+
program below and calling `http://127.0.0.1:18080/session` should return a JSON that is expected
35+
by CSI using session manager to work:
36+
37+
```shell
38+
$ curl 127.0.0.1:18080/session
39+
{"token":"cst-VCT-52f8d061-aace-4506-f4e6-fca78293a93f-....."}
40+
```
41+
42+
**NOTE**: Below implementation is **NOT PRODUCTION READY** and does not implement
43+
any kind of authentication!
44+
45+
```go
46+
package main
47+
48+
import (
49+
"context"
50+
"encoding/json"
51+
"log"
52+
"net/http"
53+
"net/url"
54+
55+
"github.com/vmware/govmomi"
56+
"github.com/vmware/govmomi/session"
57+
"github.com/vmware/govmomi/vim25"
58+
"github.com/vmware/govmomi/vim25/soap"
59+
)
60+
61+
const (
62+
vcURL = "https://my-vc.tld"
63+
vcUsername = "[email protected]"
64+
vcPassword = "somepassword"
65+
)
66+
67+
var (
68+
userPassword = url.UserPassword(vcUsername, vcPassword)
69+
)
70+
71+
// SharedSessionResponse is the expected response of CPI when using Shared session manager
72+
type SharedSessionResponse struct {
73+
Token string `json:"token"`
74+
}
75+
76+
func main() {
77+
ctx := context.Background()
78+
vcURL, err := soap.ParseURL(vcURL)
79+
if err != nil {
80+
panic(err)
81+
}
82+
soapClient := soap.NewClient(vcURL, false)
83+
c, err := vim25.NewClient(ctx, soapClient)
84+
if err != nil {
85+
panic(err)
86+
}
87+
client := &govmomi.Client{
88+
Client: c,
89+
SessionManager: session.NewManager(c),
90+
}
91+
if err := client.SessionManager.Login(ctx, userPassword); err != nil {
92+
panic(err)
93+
}
94+
95+
vcsession := func(w http.ResponseWriter, r *http.Request) {
96+
clonedtoken, err := client.SessionManager.AcquireCloneTicket(ctx)
97+
if err != nil {
98+
w.WriteHeader(http.StatusForbidden)
99+
return
100+
}
101+
token := &SharedSessionResponse{Token: clonedtoken}
102+
jsonT, err := json.Marshal(token)
103+
if err != nil {
104+
w.WriteHeader(http.StatusInternalServerError)
105+
return
106+
}
107+
w.WriteHeader(http.StatusOK)
108+
w.Write(jsonT)
109+
}
110+
111+
http.HandleFunc("/session", vcsession)
112+
log.Printf("starting webserver on port 18080")
113+
http.ListenAndServe(":18080", nil)
114+
}
115+
```

hack/release.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ function build_driver_images_linux() {
147147

148148
function build_syncer_image_linux() {
149149
echo "building ${SYNCER_IMAGE_NAME}:${VERSION} for linux"
150-
docker buildx build --platform "linux/$ARCH"\
150+
docker buildx build \
151+
--platform "linux/$ARCH" \
152+
--output "${LINUX_IMAGE_OUTPUT}" \
151153
-f images/syncer/Dockerfile \
152154
-t "${SYNCER_IMAGE_NAME}":"${VERSION}" \
153155
--build-arg "VERSION=${VERSION}" \
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package vsphere
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/vmware/govmomi/vapi/tags"
8+
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/logger"
9+
)
10+
11+
// GetTagManager returns tagManager connected to given VirtualCenter.
12+
func (vc *VirtualCenter) GetTagManager(ctx context.Context) (*tags.Manager, error) {
13+
log := logger.GetLogger(ctx)
14+
// Validate input.
15+
if vc == nil || vc.Client == nil || vc.Client.Client == nil {
16+
return nil, fmt.Errorf("vCenter not initialized")
17+
}
18+
19+
if err := vc.Connect(ctx); err != nil {
20+
return nil, fmt.Errorf("error connecting to VC: %w", err)
21+
}
22+
23+
vc.tagManager = tags.NewManager(vc.RestClient)
24+
if vc.tagManager == nil {
25+
return nil, fmt.Errorf("failed to create a tagManager")
26+
}
27+
log.Infof("New tag manager with useragent '%s'", vc.tagManager.UserAgent)
28+
return vc.tagManager, nil
29+
}

pkg/common/cns-lib/vsphere/utils.go

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@ package vsphere
22

33
import (
44
"context"
5-
"crypto/tls"
6-
"encoding/pem"
75
"errors"
86
"fmt"
9-
"net/url"
107
"reflect"
118
"strconv"
129
"strings"
1310

1411
"github.com/davecgh/go-spew/spew"
1512
"github.com/vmware/govmomi/cns"
1613
cnstypes "github.com/vmware/govmomi/cns/types"
17-
"github.com/vmware/govmomi/sts"
18-
"github.com/vmware/govmomi/vapi/rest"
19-
"github.com/vmware/govmomi/vapi/tags"
20-
"github.com/vmware/govmomi/vim25"
2114
"github.com/vmware/govmomi/vim25/soap"
2215
"github.com/vmware/govmomi/vim25/types"
2316
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/config"
@@ -201,6 +194,12 @@ func GetVirtualCenterConfig(ctx context.Context, cfg *config.Config) (*VirtualCe
201194
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
202195
MigrationDataStoreURL: cfg.VirtualCenter[host].MigrationDataStoreURL,
203196
FileVolumeActivated: cfg.VirtualCenter[host].FileVolumeActivated,
197+
VCSessionManagerURL: cfg.VirtualCenter[host].VCSessionManagerURL,
198+
VCSessionManagerToken: cfg.VirtualCenter[host].VCSessionManagerToken,
199+
}
200+
201+
if vcConfig.VCSessionManagerURL != "" {
202+
log.Infof("Using Shared Session Manager: %s", vcConfig.VCSessionManagerURL)
204203
}
205204

206205
log.Debugf("Setting the queryLimit = %v, ListVolumeThreshold = %v", vcConfig.QueryLimit, vcConfig.ListVolumeThreshold)
@@ -247,6 +246,8 @@ func GetVirtualCenterConfigs(ctx context.Context, cfg *config.Config) ([]*Virtua
247246
QueryLimit: cfg.Global.QueryLimit,
248247
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
249248
FileVolumeActivated: cfg.VirtualCenter[vCenterIP].FileVolumeActivated,
249+
VCSessionManagerURL: cfg.VirtualCenter[vCenterIP].VCSessionManagerURL,
250+
VCSessionManagerToken: cfg.VirtualCenter[vCenterIP].VCSessionManagerToken,
250251
}
251252
if vcConfig.CAFile == "" {
252253
vcConfig.CAFile = cfg.Global.CAFile
@@ -307,62 +308,6 @@ func CompareKubernetesMetadata(ctx context.Context, k8sMetaData *cnstypes.CnsKub
307308
return labelsMatch
308309
}
309310

310-
// Signer decodes the certificate and private key and returns SAML token needed
311-
// for authentication.
312-
func signer(ctx context.Context, client *vim25.Client, username string, password string) (*sts.Signer, error) {
313-
pemBlock, _ := pem.Decode([]byte(username))
314-
if pemBlock == nil {
315-
return nil, nil
316-
}
317-
certificate, err := tls.X509KeyPair([]byte(username), []byte(password))
318-
if err != nil {
319-
return nil, fmt.Errorf("failed to load X509 key pair. Error: %+v", err)
320-
}
321-
tokens, err := sts.NewClient(ctx, client)
322-
if err != nil {
323-
return nil, fmt.Errorf("failed to create STS client. err: %+v", err)
324-
}
325-
req := sts.TokenRequest{
326-
Certificate: &certificate,
327-
Delegatable: true,
328-
}
329-
signer, err := tokens.Issue(ctx, req)
330-
if err != nil {
331-
return nil, fmt.Errorf("failed to issue SAML token. err: %+v", err)
332-
}
333-
return signer, nil
334-
}
335-
336-
// GetTagManager returns tagManager connected to given VirtualCenter.
337-
func GetTagManager(ctx context.Context, vc *VirtualCenter) (*tags.Manager, error) {
338-
log := logger.GetLogger(ctx)
339-
// Validate input.
340-
if vc == nil || vc.Client == nil || vc.Client.Client == nil {
341-
return nil, fmt.Errorf("vCenter not initialized")
342-
}
343-
344-
restClient := rest.NewClient(vc.Client.Client)
345-
signer, err := signer(ctx, vc.Client.Client, vc.Config.Username, vc.Config.Password)
346-
if err != nil {
347-
return nil, fmt.Errorf("failed to create the Signer. Error: %v", err)
348-
}
349-
if signer == nil {
350-
user := url.UserPassword(vc.Config.Username, vc.Config.Password)
351-
err = restClient.Login(ctx, user)
352-
} else {
353-
err = restClient.LoginByToken(restClient.WithSigner(ctx, signer))
354-
}
355-
if err != nil {
356-
return nil, fmt.Errorf("failed to login for the rest client. Error: %v", err)
357-
}
358-
tagManager := tags.NewManager(restClient)
359-
if tagManager == nil {
360-
return nil, fmt.Errorf("failed to create a tagManager")
361-
}
362-
log.Infof("New tag manager with useragent '%s'", tagManager.UserAgent)
363-
return tagManager, nil
364-
}
365-
366311
// GetCandidateDatastoresInClusters gets the shared datastores and vSAN-direct
367312
// managed datastores of given VC clusters from GetCandidateDatastoresInCluster and
368313
// returns a map of clusterID -> array of datastores
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package vsphere
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"os"
11+
"time"
12+
)
13+
14+
const (
15+
saFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
16+
)
17+
18+
// SharedSessionResponse is the expected structure for a session manager valid
19+
// token response
20+
type SharedSessionResponse struct {
21+
Token string `json:"token"`
22+
}
23+
24+
// SharedTokenOptions represents the options that can be used when calling vc session manager
25+
type SharedTokenOptions struct {
26+
// URL is the session manager URL. Eg.: https://my-session-manager/session)
27+
URL string
28+
// Token is the authorization token that should be passed to session manager
29+
Token string
30+
// TrustedCertificates contains the certpool of certificates trusted by the client
31+
TrustedCertificates *x509.CertPool
32+
// InsecureSkipVerify defines if bad certificates requests should be ignored
33+
InsecureSkipVerify bool
34+
// Timeout defines the client timeout. Defaults to 5 seconds
35+
Timeout time.Duration
36+
// TokenFile defines a file with token content. Defaults to Kubernetes Service Account file
37+
TokenFile string
38+
}
39+
40+
// GetSharedToken executes an http request on session manager and gets the session manager
41+
// token that can be reused on govmomi sessions
42+
func GetSharedToken(ctx context.Context, options SharedTokenOptions) (string, error) {
43+
if options.URL == "" {
44+
return "", fmt.Errorf("URL of session manager cannot be empty")
45+
}
46+
47+
if options.TokenFile == "" {
48+
options.TokenFile = saFile
49+
}
50+
51+
// If the token is empty, we should use service account from the Pod instead
52+
if options.Token == "" {
53+
saValue, err := os.ReadFile(options.TokenFile)
54+
if err != nil {
55+
return "", fmt.Errorf("failed reading token from service account: %w", err)
56+
}
57+
options.Token = string(saValue)
58+
}
59+
60+
timeout := 5 * time.Second
61+
if options.Timeout != 0 {
62+
timeout = options.Timeout
63+
}
64+
65+
transport := &http.Transport{
66+
TLSClientConfig: &tls.Config{
67+
RootCAs: options.TrustedCertificates,
68+
InsecureSkipVerify: options.InsecureSkipVerify,
69+
},
70+
}
71+
72+
client := &http.Client{
73+
Timeout: timeout,
74+
Transport: transport,
75+
}
76+
77+
request, err := http.NewRequest(http.MethodGet, options.URL, nil)
78+
if err != nil {
79+
return "", fmt.Errorf("failed creating new http client: %w", err)
80+
}
81+
authToken := fmt.Sprintf("Bearer %s", options.Token)
82+
request.Header.Add("Authorization", authToken)
83+
84+
resp, err := client.Do(request)
85+
if err != nil {
86+
return "", fmt.Errorf("failed calling vc session manager: %w", err)
87+
}
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return "", fmt.Errorf("invalid vc session manager response: %s", resp.Status)
91+
}
92+
93+
token := &SharedSessionResponse{}
94+
defer resp.Body.Close()
95+
decoder := json.NewDecoder(resp.Body)
96+
if err := decoder.Decode(token); err != nil {
97+
return "", fmt.Errorf("failed decoding vc session manager response: %w", err)
98+
}
99+
100+
if token.Token == "" {
101+
return "", fmt.Errorf("returned vc session token is empty")
102+
}
103+
return token.Token, nil
104+
}

0 commit comments

Comments
 (0)