diff --git a/cluster-api/providers/vsphere/go.mod b/cluster-api/providers/vsphere/go.mod index 80b84b266e5..f652d1b93e2 100644 --- a/cluster-api/providers/vsphere/go.mod +++ b/cluster-api/providers/vsphere/go.mod @@ -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 diff --git a/cluster-api/providers/vsphere/vendor/modules.txt b/cluster-api/providers/vsphere/vendor/modules.txt index 56c8fd93fc0..3f34a290d88 100644 --- a/cluster-api/providers/vsphere/vendor/modules.txt +++ b/cluster-api/providers/vsphere/vendor/modules.txt @@ -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 diff --git a/pkg/asset/installconfig/vsphere/client.go b/pkg/asset/installconfig/vsphere/client.go index 34a229319af..5305e79f483 100644 --- a/pkg/asset/installconfig/vsphere/client.go +++ b/pkg/asset/installconfig/vsphere/client.go @@ -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" @@ -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), "<", ">\n<") + formatted = strings.ReplaceAll(formatted, " 80 { logrus.Warningf("Unable to generate ova template name due to exceeding 80 characters. Cluster=\"%v\" Failure Domain=\"%v\" results in \"%v\"", clusterID, failureDomain.Name, name) - return fmt.Errorf("ova name \"%v\" exceeed 80 characters (%d)", name, len(name)) + logrus.Errorf("ova name \"%v\" exceeed 80 characters (%d)", name, len(name)) + return nil } archive := &importer.TapeArchive{Path: cachedImage} ovfDescriptor, err := importer.ReadOvf("*.ovf", archive) if err != nil { - return debugCorruptOva(cachedImage, err) + logrus.Errorf("failed to read OVF descriptor: %v", debugCorruptOva(cachedImage, err)) + return nil } ovfEnvelope, err := importer.ReadEnvelope(ovfDescriptor) if err != nil { - return fmt.Errorf("failed to parse ovf: %w", err) + logrus.Errorf("failed to parse ovf: %v", err) + return nil } // The fcos ova enables secure boot by default, this causes @@ -90,37 +93,44 @@ func importRhcosOva(ctx context.Context, session *session.Session, folder *objec // The OVF envelope defines this. We need a 1:1 mapping // between networks with the OVF and the host if len(ovfEnvelope.Network.Networks) != 1 { - return fmt.Errorf("expected the OVA to only have a single network adapter") + logrus.Errorf("expected the OVA to only have a single network adapter") + return nil } cluster, err := session.Finder.ClusterComputeResource(ctx, failureDomain.Topology.ComputeCluster) if err != nil { - return fmt.Errorf("failed to find compute cluster: %w", err) + logrus.Errorf("failed to find compute cluster: %v", err) + return nil } clusterHostSystems, err := cluster.Hosts(ctx) if err != nil { - return fmt.Errorf("failed to get cluster hosts: %w", err) + logrus.Errorf("failed to get cluster hosts: %v", err) + return nil } if len(clusterHostSystems) == 0 { - return fmt.Errorf("the vCenter cluster %s has no ESXi nodes", failureDomain.Topology.ComputeCluster) + logrus.Errorf("the vCenter cluster %s has no ESXi nodes", failureDomain.Topology.ComputeCluster) + return nil } resourcePool, err := session.Finder.ResourcePool(ctx, failureDomain.Topology.ResourcePool) if err != nil { - return fmt.Errorf("failed to find resource pool: %w", err) + logrus.Errorf("failed to find resource pool: %v", err) + return nil } networkPath := path.Join(cluster.InventoryPath, failureDomain.Topology.Networks[0]) networkRef, err := session.Finder.Network(ctx, networkPath) if err != nil { - return fmt.Errorf("failed to find network: %w", err) + logrus.Errorf("failed to find network: %v", err) + return nil } datastore, err := session.Finder.Datastore(ctx, failureDomain.Topology.Datastore) if err != nil { - return fmt.Errorf("failed to find datastore: %w", err) + logrus.Errorf("failed to find datastore: %v", err) + return nil } // Create mapping between OVF and the network object @@ -146,7 +156,8 @@ func importRhcosOva(ctx context.Context, session *session.Session, folder *objec case "eagerZeroedThick": cisp.DiskProvisioning = string(types.OvfCreateImportSpecParamsDiskProvisioningTypeEagerZeroedThick) default: - return errors.Errorf("disk provisioning type %q is not supported", diskProvisioningType) + logrus.Errorf("disk provisioning type %q is not supported", diskProvisioningType) + return nil } m := ovf.NewManager(session.Client.Client) @@ -157,25 +168,30 @@ func importRhcosOva(ctx context.Context, session *session.Session, folder *objec &cisp) if err != nil { - return fmt.Errorf("failed to create import spec: %w", err) + logrus.Errorf("failed to create import spec: %v", err) + return nil } if spec.Error != nil { - return errors.New(spec.Error[0].LocalizedMessage) + logrus.Errorf("import spec error: %s", spec.Error[0].LocalizedMessage) + return nil } hostSystem, err := findAvailableHostSystems(ctx, clusterHostSystems, networkRef, datastore) if err != nil { - return fmt.Errorf("failed to find available host system: %w", err) + logrus.Errorf("failed to find available host system: %v", err) + return nil } lease, err := resourcePool.ImportVApp(ctx, spec.ImportSpec, folder, hostSystem) if err != nil { - return fmt.Errorf("failed to import vapp: %w", err) + logrus.Errorf("failed to import vapp: %v", err) + return nil } info, err := lease.Wait(ctx, spec.FileItem) if err != nil { - return fmt.Errorf("failed to lease wait: %w", err) + logrus.Errorf("failed to lease wait: %v", err) + return nil } u := lease.StartUpdater(ctx, info) @@ -186,40 +202,47 @@ func importRhcosOva(ctx context.Context, session *session.Session, folder *objec // available with the required network and datastore. err = upload(ctx, archive, lease, i) if err != nil { - return fmt.Errorf("failed to upload: %w", err) + logrus.Errorf("failed to upload: %v", err) + return nil } } err = lease.Complete(ctx) if err != nil { - return fmt.Errorf("failed to lease complete: %w", err) + logrus.Errorf("failed to lease complete: %v", err) + return nil } vm := object.NewVirtualMachine(session.Client.Client, info.Entity) if vm == nil { - return fmt.Errorf("error VirtualMachine not found, managed object id: %s", info.Entity.Value) + logrus.Errorf("error VirtualMachine not found, managed object id: %s", info.Entity.Value) + return nil } if secureBoot { bootOptions, err := vm.BootOptions(ctx) if err != nil { - return fmt.Errorf("failed to get boot options: %w", err) + logrus.Errorf("failed to get boot options: %v", err) + return nil } bootOptions.EfiSecureBootEnabled = ptr.To(false) err = vm.SetBootOptions(ctx, bootOptions) if err != nil { - return fmt.Errorf("failed to set boot options: %w", err) + logrus.Errorf("failed to set boot options: %v", err) + return nil } } err = vm.MarkAsTemplate(ctx) if err != nil { - return fmt.Errorf("failed to mark vm as template: %w", err) + logrus.Errorf("failed to mark vm as template: %v", err) + return nil } err = attachTag(ctx, session, vm.Reference().Value, tagID) if err != nil { - return fmt.Errorf("failed to attach tag: %w", err) + logrus.Errorf("failed to attach tag: %v", err) + return nil } return nil @@ -230,6 +253,7 @@ func findAvailableHostSystems(ctx context.Context, clusterHostSystems []*object. for _, hostObj := range clusterHostSystems { err := hostObj.Properties(ctx, hostObj.Reference(), []string{"config.product", "network", "datastore", "runtime"}, &hostSystemManagedObject) if err != nil { + logrus.Errorf("unable to get host properties: %v", err) return nil, err } @@ -246,6 +270,7 @@ func findAvailableHostSystems(ctx context.Context, clusterHostSystems []*object. logrus.Debugf("using ESXi %s to import the OVA image", hostObj.Name()) return hostObj, nil } + logrus.Errorf("all hosts unavailable") return nil, errors.New("all hosts unavailable") } diff --git a/pkg/infrastructure/vsphere/clusterapi/tags.go b/pkg/infrastructure/vsphere/clusterapi/tags.go index 3c56e89d837..012e548311e 100644 --- a/pkg/infrastructure/vsphere/clusterapi/tags.go +++ b/pkg/infrastructure/vsphere/clusterapi/tags.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/sirupsen/logrus" "github.com/vmware/govmomi/vapi/tags" "github.com/vmware/govmomi/vim25/types" "sigs.k8s.io/cluster-api-provider-vsphere/pkg/session" @@ -20,7 +21,8 @@ func attachTag(ctx context.Context, session *session.Session, vmMoRefValue, tagI err := tagManager.AttachTag(ctx, tagID, moRef) if err != nil { - return fmt.Errorf("unable to attach tag: %w", err) + logrus.Errorf("unable to attach tag: %v", err) + return nil } return nil } @@ -29,7 +31,8 @@ func createClusterTagID(ctx context.Context, session *session.Session, clusterID tagManager := session.TagManager categories, err := tagManager.GetCategories(ctx) if err != nil { - return "", fmt.Errorf("unable to get tag categories: %w", err) + logrus.Errorf("unable to get tag categories: %v", err) + return "", nil } var clusterTagCategory *tags.Category @@ -59,7 +62,8 @@ func createClusterTagID(ctx context.Context, session *session.Session, clusterID } tagCategoryID, err = tagManager.CreateCategory(ctx, clusterTagCategory) if err != nil { - return "", fmt.Errorf("unable to create tag category: %w", err) + logrus.Errorf("unable to create tag category: %v", err) + return "", nil } } @@ -68,7 +72,8 @@ func createClusterTagID(ctx context.Context, session *session.Session, clusterID categoryTags, err := tagManager.GetTagsForCategory(ctx, tagCategoryID) if err != nil { - return "", fmt.Errorf("unable to get tags for category: %w", err) + logrus.Errorf("unable to get tags for category: %v", err) + return "", nil } for i, tag := range categoryTags { if tag.Name == clusterID { @@ -86,7 +91,8 @@ func createClusterTagID(ctx context.Context, session *session.Session, clusterID } tagID, err = tagManager.CreateTag(ctx, categoryTag) if err != nil { - return "", fmt.Errorf("unable to create tag: %w", err) + logrus.Errorf("unable to create tag: %v", err) + return "", nil } } diff --git a/pkg/types/vsphere/conversion/installconfig.go b/pkg/types/vsphere/conversion/installconfig.go index 73062e22ff5..3a052cc67f8 100644 --- a/pkg/types/vsphere/conversion/installconfig.go +++ b/pkg/types/vsphere/conversion/installconfig.go @@ -2,15 +2,22 @@ package conversion import ( "context" + "crypto/tls" + "encoding/xml" "errors" "fmt" + "io" + "net/http" "net/url" "path" + "strings" "time" "github.com/sirupsen/logrus" "github.com/vmware/govmomi" "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/soap" "github.com/openshift/installer/pkg/types" @@ -28,6 +35,129 @@ const ( GeneratedFailureDomainZone string = "generated-zone" ) +// 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), "<", ">\n<") + formatted = strings.ReplaceAll(formatted, "