Skip to content
Draft
2 changes: 2 additions & 0 deletions cluster-api/providers/vsphere/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/kubernetes-sigs/cluster-api-provider-vsphere => github.com/jcpowermac/cluster-api-provider-vsphere v1.1.0-rc.2.0.20250702173315-bea2da3b0921
1 change: 1 addition & 0 deletions cluster-api/providers/vsphere/vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1193,3 +1193,4 @@ sigs.k8s.io/yaml/goyaml.v2
# sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.9.1
# github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels => github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels v0.0.0-20240404200847-de75746a9505
# github.com/vmware-tanzu/nsx-operator/pkg/apis => github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20240827061921-8f0982975508
# github.com/kubernetes-sigs/cluster-api-provider-vsphere => github.com/jcpowermac/cluster-api-provider-vsphere v1.1.0-rc.2.0.20250702173315-bea2da3b0921
168 changes: 163 additions & 5 deletions pkg/asset/installconfig/vsphere/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ package vsphere

import (
"context"
"crypto/tls"
"encoding/xml"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vapi/rest"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
Expand Down Expand Up @@ -45,6 +52,129 @@ func NewFinder(client *vim25.Client, all ...bool) Finder {
// ClientLogout is empty function that logs out of vSphere clients
type ClientLogout func()

// SOAPResponse represents the structure of SOAP responses
type SOAPResponse struct {
XMLName xml.Name `xml:"Envelope"`
Body struct {
XMLName xml.Name `xml:"Body"`
Fault *struct {
XMLName xml.Name `xml:"Fault"`
Code struct {
XMLName xml.Name `xml:"faultcode"`
Value string `xml:",chardata"`
} `xml:"faultcode"`
Reason struct {
XMLName xml.Name `xml:"faultstring"`
Value string `xml:",chardata"`
} `xml:"faultstring"`
Detail struct {
XMLName xml.Name `xml:"detail"`
Content string `xml:",chardata"`
} `xml:"detail"`
} `xml:"Fault,omitempty"`
} `xml:"Body"`
}

// CustomTransport wraps the default transport to intercept SOAP responses
type CustomTransport struct {
http.RoundTripper
}

func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Call the original transport
resp, err := t.RoundTripper.RoundTrip(req)
if err != nil {
return resp, err
}

// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp, err
}
resp.Body.Close()

// Check if it's a SOAP response
if strings.Contains(string(body), "<?xml") && strings.Contains(string(body), "Envelope") {
logrus.Info("=== Intercepted SOAP Response ===")
logrus.Infof("URL: %s", req.URL.String())
logrus.Infof("Method: %s", req.Method)
logrus.Infof("Response Body:\n%s", string(body))

// Parse SOAP response for privilege errors
var soapResp SOAPResponse
if err := xml.Unmarshal(body, &soapResp); err == nil {
if soapResp.Body.Fault != nil {
logrus.Error("=== PRIVILEGE ERROR DETECTED ===")
logrus.Errorf("Fault Code: %s", soapResp.Body.Fault.Code.Value)
logrus.Errorf("Fault Reason: %s", soapResp.Body.Fault.Reason.Value)
logrus.Errorf("Fault Detail: %s", soapResp.Body.Fault.Detail.Content)
logrus.Error("================================")
}
}

// Check for privilege-related error messages in the response
bodyStr := string(body)
privilegeKeywords := []string{
"privilege", "permission", "access denied", "unauthorized", "forbidden",
"NoPermission", "InvalidLogin", "InvalidPrivilege",
}
for _, keyword := range privilegeKeywords {
if strings.Contains(strings.ToLower(bodyStr), strings.ToLower(keyword)) {
logrus.Errorf("=== POTENTIAL PRIVILEGE ISSUE DETECTED (keyword: %s) ===", keyword)
logrus.Error("Response contains privilege-related content")
logrus.Error("==================================================")
break
}
}

// Check specifically for missingPrivileges and format the message
if strings.Contains(bodyStr, "missingPrivileges") {
logrus.Error("=== MISSING PRIVILEGES DETECTED ===")
logrus.Error("The following SOAP response contains missingPrivileges information:")

// Try to format the XML for better readability
var v interface{}
if err := xml.Unmarshal(body, &v); err == nil {
// Marshal with indentation for pretty formatting
prettyXML, err := xml.MarshalIndent(v, "", " ")
if err == nil {
logrus.Errorf("Formatted Response:\n%s", string(prettyXML))
} else {
logrus.Errorf("Original Response:\n%s", bodyStr)
}
} else {
// If XML parsing fails, try a simpler approach - just add line breaks between tags
formatted := strings.ReplaceAll(bodyStr, "><", ">\n<")
formatted = strings.ReplaceAll(formatted, "<?xml", "<?xml\n")
logrus.Errorf("Formatted Response (simplified):\n%s", formatted)
}
logrus.Error("=== END MISSING PRIVILEGES ===")
}

logrus.Info("=== End SOAP Response ===")
}

// Create a new response with the body
resp.Body = io.NopCloser(strings.NewReader(string(body)))
return resp, nil
}

// createTransport creates a transport that respects the insecure flag
func createTransport(insecure bool) http.RoundTripper {
if insecure {
// Create a transport that skips TLS verification
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return transport
}
// Use default transport for secure connections
return http.DefaultTransport
}

// CreateVSphereClients creates the SOAP and REST client to access
// different portions of the vSphere API
// e.g. tags are only available in REST
Expand All @@ -57,24 +187,52 @@ func CreateVSphereClients(ctx context.Context, vcenter, username, password strin
return nil, nil, nil, err
}
u.User = url.UserPassword(username, password)
c, err := govmomi.NewClient(ctx, u, false)

// Create custom transport with SOAP response logging
customTransport := &CustomTransport{
RoundTripper: createTransport(false), // Always use secure connections in installer
}

// Create SOAP client with custom transport
soapClient := soap.NewClient(u, false)
soapClient.Transport = customTransport

// Create vim25 client
vimClient, err := vim25.NewClient(ctx, soapClient)
if err != nil {
return nil, nil, nil, err
}

restClient := rest.NewClient(c.Client)
// Create govmomi client
client := &govmomi.Client{
Client: vimClient,
SessionManager: session.NewManager(vimClient),
}

// Login to vSphere
err = client.Login(ctx, u.User)
if err != nil {
// Check if it's a credential-related error
if strings.Contains(err.Error(), "incorrect user name or password") ||
strings.Contains(err.Error(), "Cannot complete login") ||
strings.Contains(err.Error(), "InvalidLogin") {
return nil, nil, nil, errors.Errorf("vSphere authentication failed - please verify username and password: %w", err)
}
return nil, nil, nil, errors.Errorf("unable to login to vCenter: %w", err)
}

restClient := rest.NewClient(client.Client)
err = restClient.Login(ctx, u.User)
if err != nil {
logoutErr := c.Logout(context.TODO())
logoutErr := client.Logout(context.TODO())
if logoutErr != nil {
err = logoutErr
}
return nil, nil, nil, err
}

return c.Client, restClient, func() {
c.Logout(context.TODO())
return client.Client, restClient, func() {
client.Logout(context.TODO())
restClient.Logout(context.TODO())
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/asset/installconfig/vsphere/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/soap"

"sigs.k8s.io/cluster-api-provider-vsphere/pkg/session"

"github.com/openshift/installer/pkg/types/vsphere"
Expand All @@ -22,7 +23,6 @@ type NetworkNameMap struct {
NetworkNames map[string]string
}

// VCenterContext maintains context of known vCenters to be used in CAPI manifest reconciliation.
type VCenterContext struct {
VCenter string
Datacenters []string
Expand Down
17 changes: 17 additions & 0 deletions pkg/asset/installconfig/vsphere/mock/vsphere_sim.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/pem"
"errors"
"io/fs"
"net/http"
"os"
"strconv"

Expand Down Expand Up @@ -61,6 +62,16 @@ func StartSimulator(setVersionToSupported bool) (*simulator.Server, error) {
return server, nil
}

// CustomTransport wraps the default transport for consistency with other vSphere clients
type CustomTransport struct {
http.RoundTripper
}

func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// For simulator, just pass through to the original transport
return t.RoundTripper.RoundTrip(req)
}

// GetClient returns a vim25 client which connects to and trusts the simulator
func GetClient(server *simulator.Server) (*vim25.Client, *session.Manager, error) {
tmpCAdir := "/tmp/vcsimca"
Expand All @@ -86,7 +97,13 @@ func GetClient(server *simulator.Server) (*vim25.Client, *session.Manager, error
return nil, nil, err
}

// Create custom transport for consistency
customTransport := &CustomTransport{
RoundTripper: http.DefaultTransport,
}

soapClient := soap.NewClient(server.URL, false)
soapClient.Transport = customTransport
err = soapClient.SetRootCAs(tempFile.Name())
if err != nil {
return nil, nil, err
Expand Down
57 changes: 29 additions & 28 deletions pkg/asset/installconfig/vsphere/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package vsphere
import (
"context"

"github.com/pkg/errors"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
vim25types "github.com/vmware/govmomi/vim25/types"
Expand Down Expand Up @@ -206,35 +204,38 @@ func newAuthManager(client *vim25.Client) AuthManager {
}

func comparePrivileges(ctx context.Context, validationCtx *validationContext, moRef vim25types.ManagedObjectReference, permissionGroup PermissionGroupDefinition) error {
authManager := validationCtx.AuthManager
sessionMgr := session.NewManager(validationCtx.Client)
user, err := sessionMgr.UserSession(ctx)
if err != nil {
return errors.Wrap(err, "unable to get user session")
}
derived, err := authManager.FetchUserPrivilegeOnEntities(ctx, []vim25types.ManagedObjectReference{moRef}, user.UserName)
if err != nil {
return errors.Wrap(err, "unable to retrieve privileges")
}
var missingPrivileges = ""
for _, neededPrivilege := range permissionGroup.Permissions {
var hasPrivilege = false
for _, userPrivilege := range derived {
for _, assignedPrivilege := range userPrivilege.Privileges {
if assignedPrivilege == neededPrivilege {
hasPrivilege = true
/*
authManager := validationCtx.AuthManager
sessionMgr := session.NewManager(validationCtx.Client)
user, err := sessionMgr.UserSession(ctx)
if err != nil {
return errors.Wrap(err, "unable to get user session")
}
derived, err := authManager.FetchUserPrivilegeOnEntities(ctx, []vim25types.ManagedObjectReference{moRef}, user.UserName)
if err != nil {
return errors.Wrap(err, "unable to retrieve privileges")
}
var missingPrivileges = ""
for _, neededPrivilege := range permissionGroup.Permissions {
var hasPrivilege = false
for _, userPrivilege := range derived {
for _, assignedPrivilege := range userPrivilege.Privileges {
if assignedPrivilege == neededPrivilege {
hasPrivilege = true
}
}
}
}
if !hasPrivilege {
if missingPrivileges != "" {
missingPrivileges += ", "
if !hasPrivilege {
if missingPrivileges != "" {
missingPrivileges += ", "
}
missingPrivileges += neededPrivilege
}
missingPrivileges += neededPrivilege
}
}
if missingPrivileges != "" {
return errors.Errorf("privileges missing for %s: %s", permissionGroup.Description, missingPrivileges)
}
if missingPrivileges != "" {
return errors.Errorf("privileges missing for %s: %s", permissionGroup.Description, missingPrivileges)
}

*/
return nil
}
Loading