diff --git a/go.mod b/go.mod index 36a920a65..c3fecb012 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 github.com/pkg/errors v0.9.1 - github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240422223843-6dd634aca882 - github.com/vmware/go-vcloud-director/v2 v2.21.0 + github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240426203125-2d6e9efaf23d + github.com/vmware/go-vcloud-director/v2 v2.24.0 go.uber.org/zap v1.26.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.0 diff --git a/go.sum b/go.sum index bf8abad0d..967d20ebc 100644 --- a/go.sum +++ b/go.sum @@ -344,10 +344,10 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240422223843-6dd634aca882 h1:zYn4DGeDFaE6DmoT7QEiuzfYSj0pP8eXnN1YXwtRWKU= -github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240422223843-6dd634aca882/go.mod h1:7V/gaVSTqENxOW/9GXUrY27OF7UbTSZXEqGiq75GnK4= -github.com/vmware/go-vcloud-director/v2 v2.21.0 h1:zIONrJpM+Fj+rDyXmsRfMAn1sP5WAP87USL0T9GS4DY= -github.com/vmware/go-vcloud-director/v2 v2.21.0/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw= +github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240426203125-2d6e9efaf23d h1:AJJcphXNhOLXdGAnJmNHnZ5ZRiss7xwnEOqIn725HFc= +github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240426203125-2d6e9efaf23d/go.mod h1:qhYamkHftqoYQf2dGOKafFkRD9KMbvtoJYFql76C1SU= +github.com/vmware/go-vcloud-director/v2 v2.24.0 h1:IjHISp/1Nk4bxtcA5Hx34w6J2haN/Hq66amw9XvTL54= +github.com/vmware/go-vcloud-director/v2 v2.24.0/go.mod h1:NyNcb2ymhrzwv4GyYXyYOm1NbqRwGNxDWn90AtWniXc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -380,8 +380,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/testingsdk/client.go b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/testingsdk/client.go index bd1ef35b2..9d58df960 100644 --- a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/testingsdk/client.go +++ b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/testingsdk/client.go @@ -193,6 +193,50 @@ func (tc *TestClient) GetK8sVersion() (*version.Info, error) { return tc.Cs.(*kubernetes.Clientset).Discovery().ServerVersion() } +// GetZoneMapFromZoneConfigMap reads the zone config map and returns a map of zoneName -> ovdcName +func (tc *TestClient) GetZoneMapFromZoneConfigMap(zoneCM *apiv1.ConfigMap) (map[string]string, error) { + data := zoneCM.Data + + zoneYaml, ok := data["vcloud-cse-zones.yaml"] + if !ok { + return nil, fmt.Errorf("no data present") + } + var zoneCfgMap map[string]interface{} + err := yaml.Unmarshal([]byte(zoneYaml), &zoneCfgMap) + if err != nil { + return nil, fmt.Errorf("err occurred: [%v]", err) + } + zoneInfoListIf, ok := zoneCfgMap["zones"] + if !ok { + return nil, fmt.Errorf("zone config map doesn't have zone list") + } + + zoneInfoList, ok := zoneInfoListIf.([]interface{}) + if !ok { + return nil, fmt.Errorf("failed to convert zoneInfoList interface to array") + } + + zoneMap := make(map[string]string) + for _, zoneInfoIf := range zoneInfoList { + zoneInfo, ok := zoneInfoIf.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to convert zone entry in zone config map [%v] to map[string]string", zoneInfoIf) + } + name, ok := zoneInfo["name"] + if !ok { + return nil, fmt.Errorf("zone entry in zone map doesn't have a zone name") + } + ovdcName, ok := zoneInfo["ovdcName"] + if !ok { + return nil, fmt.Errorf("zone entry in zone map doesn't have OVDC name") + } + nameStr := name.(string) + ovdcNameStr := ovdcName.(string) + zoneMap[nameStr] = ovdcNameStr + } + return zoneMap, nil +} + func (tc *TestClient) GetIpamSubnetFromConfigMap(cm *apiv1.ConfigMap) (string, error) { data := cm.Data ccmYaml, ok := data["vcloud-ccm-config.yaml"] diff --git a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/client.go b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/client.go index dab92217f..82eed4286 100644 --- a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/client.go +++ b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/client.go @@ -137,7 +137,7 @@ func (client *Client) RefreshBearerToken() error { // NewVCDClientFromSecrets : // host, orgName, userOrg, refreshToken, insecure, user, password -// New method from (vdcClient, vdcName) return *govcd.Vdc +// New method from (vdcClient, vdcIdentifier) return *govcd.Vdc func NewVCDClientFromSecrets(host string, orgName string, vdcIdentifier string, userOrg string, user string, password string, refreshToken string, insecure bool, getVdcClient bool) (*Client, error) { diff --git a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/org.go b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/org.go index 581f0fd53..180c25d28 100644 --- a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/org.go +++ b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/org.go @@ -5,7 +5,6 @@ import ( "github.com/go-openapi/errors" "github.com/vmware/go-vcloud-director/v2/govcd" "github.com/vmware/go-vcloud-director/v2/types/v56" - "k8s.io/klog/v2" "net/url" "strings" ) @@ -76,70 +75,69 @@ func (orgManager *OrgManager) GetComputePolicyDetailsFromName(computePolicyName } func (orgManager *OrgManager) SearchVMAcrossVDCs(vmName string, clusterName string, vmId string, - ovdcIdentifierList []string, isMultiZoneCluster bool) (*govcd.VM, string, error) { + isMultiZoneCluster bool) (*govcd.VM, string, error) { org, err := orgManager.Client.VCDClient.GetOrgByName(orgManager.OrgName) if err != nil { return nil, "", fmt.Errorf("unable to get org by name [%s]: [%v]", orgManager.OrgName, err) } - for _, ovdcIdentifier := range ovdcIdentifierList { - klog.Infof("Looking for VM [name:%s],[id:%s] of cluster [%s] in OVDC [%s]", - vmName, vmId, clusterName, ovdcIdentifier) + var vmRecordList []*types.QueryResultVMRecordType = nil - vdc, err := org.GetVDCByNameOrId(ovdcIdentifier, true) + if vmName != "" { + vmRecordList, err = govcd.QueryVmList(types.VmQueryFilterOnlyDeployed, &orgManager.Client.VCDClient.Client, + map[string]string{"name": vmName}) if err != nil { - klog.Infof("unable to query VDC [%s] in Org [%s] by identifier: [%v]", - ovdcIdentifier, orgManager.OrgName, err) - continue + return nil, "", fmt.Errorf("unable to query all VMs using name [%s]: [%v]", vmName, err) + } + } else if vmId != "" { + vmRecordList, err = govcd.QueryVmList(types.VmQueryFilterOnlyDeployed, &orgManager.Client.VCDClient.Client, + map[string]string{"id": vmId}) + if err != nil { + return nil, "", fmt.Errorf("unable to query all VMs using ID [%s]: [%v]", vmId, err) } - vAppNamePrefix := clusterName - if isMultiZoneCluster { - vAppNamePrefix, err = CreateVAppNamePrefix(clusterName, vdc.Vdc.ID) + } else { + return nil, "", fmt.Errorf("unable to query VM when name and ID are both not provided") + } + + for _, vmRecord := range vmRecordList { + if vmId != "" || // there is no need to correlate VM ID since it is unique across VCD + (vmName != "" && vmRecord.Name == vmName) { + vdc, err := org.GetVDCByHref(vmRecord.VdcHREF) if err != nil { - klog.Infof("Unable to create a vApp name prefix for cluster [%s] in OVDC [%s] with OVDC ID [%s]: [%v]", - clusterName, vdc.Vdc.Name, vdc.Vdc.ID, err) - continue + return nil, "", fmt.Errorf("found vm [%s, %s] in VDC with HREF[%s], but VDC could not be queried: [%v]", + vmName, vmId, vmRecord.VdcHREF, err) } - } - klog.Infof("Looking for vApps with a prefix of [%s]", vAppNamePrefix) - vAppList := vdc.GetVappList() - // check if the VM exists in any cluster-vApps in this OVDC - for _, vApp := range vAppList { - if strings.HasPrefix(vApp.Name, vAppNamePrefix) { - // check if VM exists - klog.Infof("Looking for VM [name:%s],[id:%s] in vApp [%s] in OVDC [%s] is a vApp in cluster [%s]", - vmName, vmId, vApp.Name, vdc.Vdc.Name, clusterName) - vdcManager, err := NewVDCManager(orgManager.Client, orgManager.OrgName, vdc.Vdc.Name) - if err != nil { - return nil, "", fmt.Errorf("error creating VDCManager object for VDC [%s]: [%v]", - vdc.Vdc.Name, err) - } + vm, err := orgManager.Client.VCDClient.Client.GetVMByHref(vmRecord.HREF) + if err != nil { + return nil, "", fmt.Errorf("unable to find VM [%s, %s] by HREF [%s]: [%v]", + vmName, vmId, vmRecord.HREF, err) + } - var vm *govcd.VM = nil - if vmName != "" { - vm, err = vdcManager.FindVMByName(vApp.Name, vmName) - } else if vmId != "" { - vm, err = vdcManager.FindVMByUUID(vApp.Name, vmId) - } else { - return nil, "", fmt.Errorf("either vm name [%s] or ID [%s] should be passed", vmName, vmId) + vApp, err := vm.GetParentVApp() + if err != nil { + return nil, "", fmt.Errorf("unable to get parent of VM [%s, %s]: [%v]", vmName, vmId, err) + } + + if isMultiZoneCluster { + vdcUUID := vdc.Vdc.ID + vdcIdParts := strings.Split(vdc.Vdc.ID, ":") + if len(vdcIdParts) == 4 { + vdcUUID = vdcIdParts[3] } - if err != nil { - klog.Infof("Could not find VM [name:%s],[id:%s] in vApp [%s] of Cluster [%s] in OVDC [%s]: [%v]", - vmName, vmId, vApp.Name, clusterName, vdc.Vdc.Name, err) + // In this case we need to check if the vApp Name is a proper prefix + if !strings.HasPrefix(vApp.VApp.Name, fmt.Sprintf("%s_%s", clusterName, vdcUUID)) { + continue + } + } else { + if vApp.VApp.Name != clusterName { continue } - - // If we reach here, we found the VM - klog.Infof("Found VM [name:%s],[id:%s] in vApp [%s] of Cluster [%s] in OVDC [%s]: [%v]", - vmName, vmId, vApp.Name, clusterName, vdc.Vdc.Name, err) - return vm, vdc.Vdc.Name, nil } - } - klog.Infof("Could not find VM [name:%s],[id:%s] of cluster [%s] in OVDC [%s]", - vmName, vmId, clusterName, ovdcIdentifier) + return vm, vdc.Vdc.Name, nil + } } return nil, "", govcd.ErrorEntityNotFound diff --git a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/vapp.go b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/vapp.go index cb7140d94..4570909fa 100644 --- a/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/vapp.go +++ b/vendor/github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk/vapp.go @@ -266,17 +266,11 @@ func (vdc *VdcManager) GetOrCreateVApp(VAppName string, ovdcNetworkName string) } // vapp not found, so create one - err = vdc.Vdc.ComposeRawVApp(VAppName, fmt.Sprintf("Description for [%s]", VAppName)) + vApp, err = vdc.Vdc.CreateRawVApp(VAppName, fmt.Sprintf("Description for [%s]", VAppName)) if err != nil { return nil, fmt.Errorf("unable to compose raw vApp with name [%s]: [%v]", VAppName, err) } - vApp, err = vdc.Vdc.GetVAppByName(VAppName, true) - if err != nil { - return nil, fmt.Errorf("unable to get vApp [%s] from Vdc [%s]: [%v]", - VAppName, vdc.Client.ClusterOVDCIdentifier, err) - } - if err := vdc.addOvdcNetworkToVApp(vApp, ovdcNetworkName); err != nil { return nil, fmt.Errorf("unable to add ovdc network [%s] to vApp [%s]: [%v]", ovdcNetworkName, VAppName, err) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/access_control.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/access_control.go index 8d0593a63..140722e4f 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/access_control.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/access_control.go @@ -456,3 +456,145 @@ func checkSanityVdcControlAccess(vdc *Vdc) error { } return nil } + +func publishCatalog(client *Client, catalogUrl string, tenantContext *TenantContext, publishCatalog types.PublishCatalogParams) error { + catalogUrl = catalogUrl + "/action/publish" + + publishCatalog.Xmlns = types.XMLNamespaceVCloud + + if tenantContext != nil { + client.SetCustomHeader(getTenantContextHeader(tenantContext)) + } + + err := client.ExecuteRequestWithoutResponse(catalogUrl, http.MethodPost, + types.PublishCatalog, "error setting catalog publishing state: %s", publishCatalog) + + if tenantContext != nil { + client.RemoveProvidedCustomHeaders(getTenantContextHeader(tenantContext)) + } + + return err +} + +// IsSharedReadOnly returns the state of the catalog read-only sharing to all organizations +func (cat *Catalog) IsSharedReadOnly() (bool, error) { + accessControl, err := cat.GetAccessControl(true) + if err != nil { + return false, err + } + if accessControl.AccessSettings != nil || accessControl.IsSharedToEveryone { + return false, nil + } + err = cat.Refresh() + if err != nil { + return false, err + } + return cat.Catalog.IsPublished, nil +} + +// IsSharedReadOnly returns the state of the catalog read-only sharing to all organizations +func (cat *AdminCatalog) IsSharedReadOnly() (bool, error) { + accessControl, err := cat.GetAccessControl(true) + if err != nil { + return false, err + } + if accessControl.AccessSettings != nil || accessControl.IsSharedToEveryone { + return false, nil + } + err = cat.Refresh() + if err != nil { + return false, err + } + return cat.AdminCatalog.IsPublished, nil +} + +// publish publishes a catalog read-only access control to all organizations. +// This operation is usually the second step for a read-only sharing to all Orgs +func (cat *Catalog) publish(isPublished bool) error { + if cat.Catalog == nil { + return fmt.Errorf("cannot publish catalog, Object is empty") + } + + catalogUrl := cat.Catalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { + return fmt.Errorf("cannot publish catalog, HREF is empty") + } + + tenantContext, err := cat.getTenantContext() + if err != nil { + return fmt.Errorf("cannot publish catalog, tenant context error: %s", err) + } + + publishParameters := types.PublishCatalogParams{ + IsPublished: &isPublished, + } + err = publishCatalog(cat.client, catalogUrl, tenantContext, publishParameters) + if err != nil { + return err + } + + return cat.Refresh() +} + +// publish publishes a catalog read-only access control to all organizations. +// This operation is usually the second step for a read-only sharing to all Orgs +func (cat *AdminCatalog) publish(isPublished bool) error { + if cat.AdminCatalog == nil { + return fmt.Errorf("cannot publish catalog, Object is empty") + } + + catalogUrl := cat.AdminCatalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { + return fmt.Errorf("cannot publish catalog, HREF is empty") + } + + tenantContext, err := cat.getTenantContext() + if err != nil { + return fmt.Errorf("cannot publish catalog, tenant context error: %s", err) + } + + publishParameters := types.PublishCatalogParams{ + IsPublished: &isPublished, + } + err = publishCatalog(cat.client, catalogUrl, tenantContext, publishParameters) + if err != nil { + return err + } + + err = cat.Refresh() + if err != nil { + return err + } + + return err +} + +// SetReadOnlyAccessControl will create or rescind the read-only catalog sharing to all organizations +func (cat *Catalog) SetReadOnlyAccessControl(isPublished bool) error { + if cat.Catalog == nil { + return fmt.Errorf("cannot set access control, Object is empty") + } + err := cat.SetAccessControl(&types.ControlAccessParams{ + IsSharedToEveryone: false, + EveryoneAccessLevel: addrOf(types.ControlAccessReadOnly), + }, true) + if err != nil { + return fmt.Errorf("error resetting access control record for catalog %s: %s", cat.Catalog.Name, err) + } + return cat.publish(isPublished) +} + +// SetReadOnlyAccessControl will create or rescind the read-only AdminCatalog sharing to all organizations +func (cat *AdminCatalog) SetReadOnlyAccessControl(isPublished bool) error { + if cat.AdminCatalog == nil { + return fmt.Errorf("cannot set access control, Object is empty") + } + err := cat.SetAccessControl(&types.ControlAccessParams{ + IsSharedToEveryone: false, + EveryoneAccessLevel: addrOf(types.ControlAccessReadOnly), + }, true) + if err != nil { + return err + } + return cat.publish(isPublished) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go index a6c60fb8b..9585249c5 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/admincatalog.go @@ -624,6 +624,20 @@ func (client *Client) GetAdminCatalogByHref(catalogHref string) (*AdminCatalog, return nil, err } + // Setting the catalog parent, necessary to handle the tenant context + org := NewAdminOrg(client) + for _, link := range cat.AdminCatalog.Link { + if link.Rel == "up" && link.Type == types.MimeAdminOrg { + _, err = client.ExecuteRequest(link.HREF, http.MethodGet, + "", "error retrieving parent Org: %s", nil, org.AdminOrg) + if err != nil { + return nil, fmt.Errorf("error retrieving catalog parent: %s", err) + } + break + } + } + + cat.parent = org return cat, nil } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go index 0b58ccf9b..943bcea30 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminorg.go @@ -35,7 +35,7 @@ func NewAdminOrg(cli *Client) *AdminOrg { } } -// CreateCatalog creates a catalog with given name and description under the +// CreateCatalog creates a catalog with given name and description under // the given organization. Returns an AdminCatalog that contains a creation // task. // API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html @@ -45,6 +45,20 @@ func (adminOrg *AdminOrg) CreateCatalog(name, description string) (AdminCatalog, return AdminCatalog{}, err } adminCatalog.parent = adminOrg + + err = adminCatalog.Refresh() + if err != nil { + return AdminCatalog{}, err + } + // Make sure that the creation task is finished + err = adminCatalog.WaitForTasks() + if err != nil { + return AdminCatalog{}, err + } + err = adminCatalog.WaitForTasks() + if err != nil { + return AdminCatalog{}, err + } return *adminCatalog, nil } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go index 4a461b82a..09087a9c8 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/adminvdc.go @@ -173,6 +173,9 @@ func (adminOrg *AdminOrg) CreateVdc(vdcConfiguration *types.VdcConfiguration) (T // Return the task task := NewTask(adminOrg.client) + if adminVdc.AdminVdc.Tasks == nil || len(adminVdc.AdminVdc.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after VDC %s creation", vdcConfiguration.Name) + } task.Task = adminVdc.AdminVdc.Tasks.Task[0] return *task, nil } @@ -376,6 +379,9 @@ func createVdcAsyncV97(adminOrg *AdminOrg, vdcConfiguration *types.VdcConfigurat // Return the task task := NewTask(adminOrg.client) + if adminVdc.AdminVdc.Tasks == nil || len(adminVdc.AdminVdc.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after VDC %s creation", vdcConfiguration.Name) + } task.Task = adminVdc.AdminVdc.Tasks.Task[0] return *task, nil } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api_vcd.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api_vcd.go index 5fcc94b6d..b9e94143d 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api_vcd.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api_vcd.go @@ -119,7 +119,7 @@ func (vcdClient *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http. // NewVCDClient initializes VMware VMware Cloud Director client with reasonable defaults. // It accepts functions of type VCDClientOption for adjusting defaults. func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption) *VCDClient { - minVcdApiVersion := "36.0" // supported by 10.3+ + minVcdApiVersion := "37.0" // supported by 10.4+ userDefinedApiVersion := os.Getenv("GOVCD_API_VERSION") if userDefinedApiVersion != "" { _, err := semver.NewVersion(userDefinedApiVersion) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go index ab65c4974..0406505f5 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go @@ -59,7 +59,7 @@ func (catalog *Catalog) Delete(force, recursive bool) error { // A catalog cannot be removed if it has active tasks, or if any of its items have active tasks err = catalog.consumeTasks() if err != nil { - return err + return fmt.Errorf("error while consuming tasks from catalog %s: %s", catalog.Catalog.Name, err) } } @@ -94,7 +94,7 @@ func (catalog *Catalog) consumeTasks() error { "status": "running,preRunning,queued", }) if err != nil { - return err + return fmt.Errorf("error getting task list from catalog %s: %s", catalog.Catalog.Name, err) } var taskList []string addTask := func(status, href string) { @@ -119,7 +119,7 @@ func (catalog *Catalog) consumeTasks() error { } catalogItemRefs, err := catalog.QueryCatalogItemList() if err != nil { - return err + return fmt.Errorf("error getting catalog %s items list: %s", catalog.Catalog.Name, err) } for _, task := range allTasks { for _, ref := range catalogItemRefs { @@ -131,7 +131,10 @@ func (catalog *Catalog) consumeTasks() error { } } _, err = catalog.client.WaitTaskListCompletion(taskList, true) - return err + if err != nil { + return fmt.Errorf("error while waiting for task list completion for catalog %s: %s", catalog.Catalog.Name, err) + } + return nil } // Envelope is a ovf description root element. File contains information for vmdk files. @@ -357,6 +360,53 @@ func (cat *Catalog) UploadOvfByLink(ovfUrl, itemName, description string) (Task, return task, nil } +// CaptureVappTemplate captures a vApp template from an existing vApp +func (cat *Catalog) CaptureVappTemplate(captureParams *types.CaptureVAppParams) (*VAppTemplate, error) { + task, err := cat.CaptureVappTemplateAsync(captureParams) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + + if task.Task == nil || task.Task.Owner == nil || task.Task.Owner.HREF == "" { + return nil, fmt.Errorf("task does not have Owner HREF populated: %#v", task) + } + + // After the task is finished, owner field contains the resulting vApp template + return cat.GetVappTemplateByHref(task.Task.Owner.HREF) +} + +// CaptureVappTemplateAsync triggers vApp template capturing task and returns it +// +// Note. If 'CaptureVAppParams.CopyTpmOnInstantiate' is set, it will be unset for VCD versions +// before 10.4.2 as it would break API call +func (cat *Catalog) CaptureVappTemplateAsync(captureParams *types.CaptureVAppParams) (Task, error) { + util.Logger.Printf("[TRACE] Capturing vApp template to catalog %s", cat.Catalog.Name) + if captureParams == nil { + return Task{}, fmt.Errorf("input CaptureVAppParams cannot be nil") + } + + captureTemplateHref := cat.client.VCDHREF + captureTemplateHref.Path += fmt.Sprintf("/catalog/%s/action/captureVApp", extractUuid(cat.Catalog.ID)) + + captureParams.Xmlns = types.XMLNamespaceVCloud + captureParams.XmlnsNs0 = types.XMLNamespaceOVF + + util.Logger.Printf("[TRACE] Url for capturing vApp template: %s", captureTemplateHref.String()) + + if cat.client.APIVCDMaxVersionIs("< 37.2") { + captureParams.CopyTpmOnInstantiate = nil + util.Logger.Println("[TRACE] Explicitly unsetting 'CopyTpmOnInstantiate' because it was not supported before VCD 10.4.2") + } + + return cat.client.ExecuteTaskRequest(captureTemplateHref.String(), http.MethodPost, + types.MimeCaptureVappTemplateParams, "error capturing vApp Template: %s", captureParams) +} + // Upload files for vCD created upload links. Different approach then vmdk file are // chunked (e.g. test.vmdk.000000000, test.vmdk.000000001 or test.vmdk). vmdk files are chunked if // in description file attribute ChunkSize is not zero. @@ -827,7 +877,14 @@ func removeCatalogItemOnError(client *Client, vappTemplateLink *url.URL, itemNam } } +// UploadMediaImage uploads a media image to the catalog func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath string, uploadPieceSize int64) (UploadTask, error) { + return cat.UploadMediaFile(mediaName, mediaDescription, filePath, uploadPieceSize, true) +} + +// UploadMediaFile uploads any file to the catalog. +// However, if checkFileIsIso is true, only .ISO are allowed. +func (cat *Catalog) UploadMediaFile(fileName, mediaDescription, filePath string, uploadPieceSize int64, checkFileIsIso bool) (UploadTask, error) { if *cat == (Catalog{}) { return UploadTask{}, errors.New("catalog can not be empty or nil") @@ -838,9 +895,11 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin return UploadTask{}, err } - isISOGood, err := verifyIso(mediaFilePath) - if err != nil || !isISOGood { - return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err) + if checkFileIsIso { + isISOGood, err := verifyIso(mediaFilePath) + if err != nil || !isISOGood { + return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err) + } } file, e := os.Stat(mediaFilePath) @@ -850,8 +909,8 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin fileSize := file.Size() for _, catalogItemName := range getExistingCatalogItems(cat) { - if catalogItemName == mediaName { - return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", mediaName) + if catalogItemName == fileName { + return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", fileName) } } @@ -860,17 +919,17 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin return UploadTask{}, err } - media, err := createMedia(cat.client, catalogItemUploadURL.String(), mediaName, mediaDescription, fileSize) + media, err := createMedia(cat.client, catalogItemUploadURL.String(), fileName, mediaDescription, fileSize) if err != nil { return UploadTask{}, fmt.Errorf("[ERROR] Issue creating media: %#v", err) } - createdMedia, err := queryMedia(cat.client, media.Entity.HREF, mediaName) + createdMedia, err := queryMedia(cat.client, media.Entity.HREF, fileName) if err != nil { return UploadTask{}, err } - return executeUpload(cat.client, createdMedia, mediaFilePath, mediaName, fileSize, uploadPieceSize) + return executeUpload(cat.client, createdMedia, mediaFilePath, fileName, fileSize, uploadPieceSize) } // Refresh gets a fresh copy of the catalog from vCD @@ -1079,12 +1138,12 @@ func (cat *Catalog) PublishToExternalOrganizations(publishExternalCatalog types. return fmt.Errorf("cannot publish to external organization, Object is empty") } - url := cat.Catalog.HREF - if url == "nil" || url == "" { + catalogUrl := cat.Catalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { return fmt.Errorf("cannot publish to external organization, HREF is empty") } - err := publishToExternalOrganizations(cat.client, url, nil, publishExternalCatalog) + err := publishToExternalOrganizations(cat.client, catalogUrl, nil, publishExternalCatalog) if err != nil { return err } @@ -1171,7 +1230,19 @@ func (client *Client) GetCatalogByHref(catalogHref string) (*Catalog, error) { if err != nil { return nil, err } - + // Setting the catalog parent, necessary to handle the tenant context + org := NewOrg(client) + for _, link := range cat.Catalog.Link { + if link.Rel == "up" && link.Type == types.MimeOrg { + _, err = client.ExecuteRequest(link.HREF, http.MethodGet, + "", "error retrieving parent Org: %s", nil, org.Org) + if err != nil { + return nil, fmt.Errorf("error retrieving catalog parent: %s", err) + } + break + } + } + cat.parent = org return cat, nil } @@ -1205,3 +1276,18 @@ func (client *Client) GetCatalogByName(parentOrg, catalogName string) (*Catalog, } return nil, fmt.Errorf("no catalog '%s' found in Org %s%s", catalogName, parentOrg, parents) } + +// WaitForTasks waits for the catalog's tasks to complete +func (cat *Catalog) WaitForTasks() error { + if ResourceInProgress(cat.Catalog.Tasks) { + err := WaitResource(func() (*types.TasksInProgress, error) { + err := cat.Refresh() + if err != nil { + return nil, err + } + return cat.Catalog.Tasks, nil + }) + return err + } + return nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalogitem.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalogitem.go index 33b48944b..fe321e382 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalogitem.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalogitem.go @@ -108,9 +108,12 @@ func queryVappTemplateListWithFilter(client *Client, filter map[string]string) ( for k, v := range filter { filterEncoded += fmt.Sprintf("%s==%s;", url.QueryEscape(k), url.QueryEscape(v)) } + if len(filterEncoded) > 0 { + filterEncoded = filterEncoded[:len(filterEncoded)-1] // Removes the trailing ';' + } results, err := client.cumulativeQuery(vappTemplateType, nil, map[string]string{ "type": vappTemplateType, - "filter": filterEncoded[:len(filterEncoded)-1], // Removes the trailing ';' + "filter": filterEncoded, }) if err != nil { return nil, fmt.Errorf("error querying vApp templates %s", err) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse.go new file mode 100644 index 000000000..305e85ec6 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse.go @@ -0,0 +1,394 @@ +package govcd + +import ( + "encoding/json" + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "strings" + "time" +) + +// CseCreateKubernetesCluster creates a Kubernetes cluster with the data given as input (CseClusterSettings). If the given +// timeout is 0, it waits forever for the cluster creation. +// +// If the timeout is reached and the cluster is not available (in "provisioned" state), it will return a non-nil CseKubernetesCluster +// with only the cluster ID and an error. This means that the cluster will be left in VCD in any state, and it can be retrieved afterward +// with Org.CseGetKubernetesClusterById and the returned ID. +// +// If the cluster is created correctly, returns all the available data in CseKubernetesCluster or an error if some of the fields +// of the created cluster cannot be calculated or retrieved. +func (org *Org) CseCreateKubernetesCluster(clusterData CseClusterSettings, timeout time.Duration) (*CseKubernetesCluster, error) { + clusterId, err := org.CseCreateKubernetesClusterAsync(clusterData) + if err != nil { + return nil, err + } + + err = waitUntilClusterIsProvisioned(org.client, clusterId, timeout) + if err != nil { + return &CseKubernetesCluster{ + client: org.client, + ID: clusterId, + }, err + } + + return getCseKubernetesClusterById(org.client, clusterId) +} + +// CseCreateKubernetesClusterAsync creates a Kubernetes cluster with the data given as input (CseClusterSettings), but does not +// wait for the creation process to finish, so it doesn't monitor for any errors during the process. It returns just the ID of +// the created cluster. One can manually check the status of the cluster with VCDClient.CseGetKubernetesClusterById and the result of this method. +func (org *Org) CseCreateKubernetesClusterAsync(clusterSettings CseClusterSettings) (string, error) { + if org == nil { + return "", fmt.Errorf("CseCreateKubernetesClusterAsync cannot be called on a nil Organization receiver") + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + cseSubcomponents, err := getCseComponentsVersions(clusterSettings.CseVersion) + if err != nil { + return "", err + } + + internalSettings, err := clusterSettings.toCseClusterSettingsInternal(*org) + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + payload, err := internalSettings.getUnmarshalledRdePayload() + if err != nil { + return "", err + } + + rde, err := createRdeAndGetFromTask(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseSubcomponents.CapvcdRdeTypeVersion, + types.DefinedEntity{ + EntityType: internalSettings.RdeType.ID, + Name: internalSettings.Name, + Entity: payload, + }, tenantContext) + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + return rde.DefinedEntity.ID, nil +} + +// CseGetKubernetesClusterById retrieves a CSE Kubernetes cluster from VCD by its unique ID +func (vcdClient *VCDClient) CseGetKubernetesClusterById(id string) (*CseKubernetesCluster, error) { + return getCseKubernetesClusterById(&vcdClient.Client, id) +} + +// CseGetKubernetesClustersByName retrieves all the CSE Kubernetes clusters from VCD with the given name that belong to the receiver Organization. +// Note: The clusters retrieved won't have a valid ETag to perform operations on them. Use VCDClient.CseGetKubernetesClusterById for that instead. +func (org *Org) CseGetKubernetesClustersByName(cseVersion semver.Version, name string) ([]*CseKubernetesCluster, error) { + cseSubcomponents, err := getCseComponentsVersions(cseVersion) + if err != nil { + return nil, err + } + + rdes, err := getRdesByName(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseSubcomponents.CapvcdRdeTypeVersion, name) + if err != nil { + return nil, err + } + var clusters []*CseKubernetesCluster + for _, rde := range rdes { + if rde.DefinedEntity.Org != nil && rde.DefinedEntity.Org.ID == org.Org.ID { + cluster, err := cseConvertToCseKubernetesClusterType(rde) + if err != nil { + return nil, err + } + clusters = append(clusters, cluster) + } + } + return clusters, nil +} + +// getCseKubernetesClusterById retrieves a CSE Kubernetes cluster from VCD by its unique ID +func getCseKubernetesClusterById(client *Client, clusterId string) (*CseKubernetesCluster, error) { + rde, err := getRdeById(client, clusterId) + if err != nil { + return nil, err + } + return cseConvertToCseKubernetesClusterType(rde) +} + +// Refresh gets the latest information about the receiver CSE Kubernetes cluster and updates its properties. +// All cached fields such as the supported OVAs list (from CseKubernetesCluster.GetSupportedUpgrades) are also cleared. +func (cluster *CseKubernetesCluster) Refresh() error { + refreshed, err := getCseKubernetesClusterById(cluster.client, cluster.ID) + if err != nil { + return fmt.Errorf("failed refreshing the CSE Kubernetes Cluster: %s", err) + } + *cluster = *refreshed + return nil +} + +// GetKubeconfig retrieves the Kubeconfig from an existing CSE Kubernetes cluster that is in provisioned state. +// If refresh=true, it retrieves the latest state of the cluster from VCD before requesting the Kubeconfig. +func (cluster *CseKubernetesCluster) GetKubeconfig(refresh bool) (string, error) { + if refresh { + err := cluster.Refresh() + if err != nil { + return "", err + } + } + + if cluster.State == "" { + return "", fmt.Errorf("cannot get a Kubeconfig of a Kubernetes cluster that does not have a state (expected 'provisioned')") + } + + if cluster.State != "provisioned" { + return "", fmt.Errorf("cannot get a Kubeconfig of a Kubernetes cluster that is not in 'provisioned' state. It is '%s'", cluster.State) + } + + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + return "", err + } + versions, err := getCseComponentsVersions(cluster.CseVersion) + if err != nil { + return "", err + } + + // Auxiliary wrapper of the result, as the invocation returns the RDE and + // what we need is inside of it. + type invocationResult struct { + Capvcd types.Capvcd `json:"entity,omitempty"` + } + result := invocationResult{} + + err = rde.InvokeBehaviorAndMarshal(fmt.Sprintf("urn:vcloud:behavior-interface:getFullEntity:cse:capvcd:%s", versions.CseInterfaceVersion), types.BehaviorInvocation{}, &result) + if err != nil { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation failed: %s", err) + } + if result.Capvcd.Status.Capvcd.Private == nil { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation succeeded but the Kubeconfig is nil") + } + if result.Capvcd.Status.Capvcd.Private.KubeConfig == "" { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation succeeded but the Kubeconfig is empty") + } + return result.Capvcd.Status.Capvcd.Private.KubeConfig, nil +} + +// UpdateWorkerPools executes an update on the receiver cluster to change the existing Worker Pools. +// The input is a map where the key is the Worker pool unique name, and the value is the update payload for that Worker Pool. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +// WARNING: At least one worker pool must have one or more nodes running, otherwise the cluster will be left in an unusable state. +func (cluster *CseKubernetesCluster) UpdateWorkerPools(input map[string]CseWorkerPoolUpdateInput, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + WorkerPools: &input, + }, refresh) +} + +// AddWorkerPools executes an update on the receiver cluster to add new Worker Pools. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) AddWorkerPools(input []CseWorkerPoolSettings, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + NewWorkerPools: &input, + }, refresh) +} + +// UpdateControlPlane executes an update on the receiver cluster to change the existing control plane. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) UpdateControlPlane(input CseControlPlaneUpdateInput, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + ControlPlane: &input, + }, refresh) +} + +// GetSupportedUpgrades queries all vApp Templates from VCD, one by one, and returns those that can be used for upgrading the cluster. +// As retrieving all OVAs one by one from VCD is expensive, the first time this method is called the returned OVAs are +// cached to avoid querying VCD again multiple times. +// If refreshOvas=true, this cache is cleared out and this method will query VCD for every vApp Template again. +// Therefore, the refreshOvas flag should be set to true only when VCD has new OVAs that need to be considered or after a cluster upgrade. +// NOTE: Any refresh operation from other methods will cause the cache to be cleared. +func (cluster *CseKubernetesCluster) GetSupportedUpgrades(refreshOvas bool) ([]*types.VAppTemplate, error) { + if refreshOvas { + cluster.supportedUpgrades = make([]*types.VAppTemplate, 0) + } + if cluster.State != "provisioned" { + cluster.supportedUpgrades = make([]*types.VAppTemplate, 0) + return cluster.supportedUpgrades, nil + } + if len(cluster.supportedUpgrades) > 0 { + return cluster.supportedUpgrades, nil + } + + vAppTemplates, err := queryVappTemplateListWithFilter(cluster.client, nil) + if err != nil { + return nil, fmt.Errorf("could not get vApp Templates: %s", err) + } + for _, template := range vAppTemplates { + // We can only know if the vApp Template is a TKGm OVA by inspecting its internals, hence we need to retrieve every one + // of them one by one. This is an expensive operation, hence the cache. + vAppTemplate, err := getVAppTemplateById(cluster.client, fmt.Sprintf("urn:vcloud:vapptemplate:%s", extractUuid(template.HREF))) + if err != nil { + continue // This means we cannot retrieve it (maybe due to some rights missing), so we cannot use it. We skip it + } + targetVersions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + continue // This means it's not a TKGm OVA, or it is not supported, so we skip it + } + // The OVA can be used if the TKG version is equal to the actual or higher, and the Kubernetes version is at most 1 minor higher. + if targetVersions.compareTkgVersion(cluster.TkgVersion.String()) >= 0 && targetVersions.kubernetesVersionIsUpgradeableFrom(cluster.KubernetesVersion.String()) { + cluster.supportedUpgrades = append(cluster.supportedUpgrades, vAppTemplate.VAppTemplate) + } + } + return cluster.supportedUpgrades, nil +} + +// UpgradeCluster executes an update on the receiver cluster to upgrade the Kubernetes template of the cluster. +// If the cluster is not upgradeable or the OVA is incorrect, this method will return an error. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) UpgradeCluster(kubernetesTemplateOvaId string, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + KubernetesTemplateOvaId: &kubernetesTemplateOvaId, + }, refresh) +} + +// SetNodeHealthCheck executes an update on the receiver cluster to enable or disable the machine health check capabilities. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) SetNodeHealthCheck(healthCheckEnabled bool, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + NodeHealthCheck: &healthCheckEnabled, + }, refresh) +} + +// SetAutoRepairOnErrors executes an update on the receiver cluster to change the flag that controls the auto-repair +// capabilities of CSE. If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +// NOTE: This method can only be used in CSE versions < 4.1.1 +func (cluster *CseKubernetesCluster) SetAutoRepairOnErrors(autoRepairOnErrors bool, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + AutoRepairOnErrors: &autoRepairOnErrors, + }, refresh) +} + +// Update executes an update on the receiver CSE Kubernetes Cluster on any of the allowed parameters defined in the input type. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) Update(input CseClusterUpdateInput, refresh bool) error { + if refresh { + err := cluster.Refresh() + if err != nil { + return err + } + } + + if cluster.State == "" { + return fmt.Errorf("can't update a Kubernetes cluster that does not have any state") + } + if cluster.State != "provisioned" { + return fmt.Errorf("can't update a Kubernetes cluster that is not in 'provisioned' state, as it is in '%s'", cluster.capvcdType.Status.VcdKe.State) + } + + if input.AutoRepairOnErrors != nil && *input.AutoRepairOnErrors != cluster.AutoRepairOnErrors { + // Since CSE 4.1.1, the AutoRepairOnError toggle can't be modified and is turned off + // automatically by the CSE Server. + + v411, err := semver.NewVersion("4.1.1") + if err != nil { + return err + } + if cluster.CseVersion.GreaterThanOrEqual(v411) { + return fmt.Errorf("the 'Auto Repair on Errors' flag can't be changed after the cluster is created since CSE 4.1.1") + } + cluster.capvcdType.Spec.VcdKe.AutoRepairOnErrors = *input.AutoRepairOnErrors + } + + updatedCapiYaml, err := cluster.updateCapiYaml(input) + if err != nil { + return err + } + cluster.capvcdType.Spec.CapiYaml = updatedCapiYaml + + marshaledPayload, err := json.Marshal(cluster.capvcdType) + if err != nil { + return err + } + entityContent := map[string]interface{}{} + err = json.Unmarshal(marshaledPayload, &entityContent) + if err != nil { + return err + } + + // We do this loop to increase the chances that the Kubernetes cluster is successfully updated, as the update operation + // can clash with the CSE Server updates on the same RDE. If the CSE Server does an update just before we do, the ETag + // verification will fail, so we must retry. + retries := 0 + maxRetries := 5 + updated := false + for retries <= maxRetries { + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + return err + } + + rde.DefinedEntity.Entity = entityContent + err = rde.Update(*rde.DefinedEntity) + if err == nil { + updated = true + break + } + if err != nil { + // If it's an ETag error, we just retry without waiting + if !strings.Contains(strings.ToLower(err.Error()), "etag") { + return err + } + } + retries++ + util.Logger.Printf("[DEBUG] The request to update the Kubernetes cluster '%s' failed due to a ETag lock. Trying again", cluster.ID) + } + + if !updated { + return fmt.Errorf("could not update the Kubernetes cluster '%s' after %d retries, due to an ETag lock blocking the operations", cluster.ID, maxRetries) + } + + return cluster.Refresh() +} + +// Delete deletes a CSE Kubernetes cluster, waiting the specified amount of time. If the timeout is reached, this method +// returns an error, even if the cluster is already marked for deletion. +func (cluster *CseKubernetesCluster) Delete(timeout time.Duration) error { + var elapsed time.Duration + start := time.Now() + markForDelete := false + forceDelete := false + for elapsed <= timeout || timeout == 0 { // If the user specifies timeout=0, we wait forever + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + if ContainsNotFound(err) { + return nil // The RDE is gone, so the process is completed and there's nothing more to do + } + return fmt.Errorf("could not retrieve the Kubernetes cluster with ID '%s': %s", cluster.ID, err) + } + + markForDelete = traverseMapAndGet[bool](rde.DefinedEntity.Entity, "spec.vcdKe.markForDelete") + forceDelete = traverseMapAndGet[bool](rde.DefinedEntity.Entity, "spec.vcdKe.forceDelete") + + if !markForDelete || !forceDelete { + // Mark the cluster for deletion + rde.DefinedEntity.Entity["spec"].(map[string]interface{})["vcdKe"].(map[string]interface{})["markForDelete"] = true + rde.DefinedEntity.Entity["spec"].(map[string]interface{})["vcdKe"].(map[string]interface{})["forceDelete"] = true + err = rde.Update(*rde.DefinedEntity) + if err != nil { + // We ignore any ETag error. This just means a clash with the CSE Server, we just try again + if !strings.Contains(strings.ToLower(err.Error()), "etag") { + return fmt.Errorf("could not mark the Kubernetes cluster with ID '%s' to be deleted: %s", cluster.ID, err) + } + } + } + + util.Logger.Printf("[DEBUG] Cluster '%s' is still not deleted, will check again in 10 seconds", cluster.ID) + time.Sleep(10 * time.Second) + elapsed = time.Since(start) + } + + // We give a hint to the user about the deletion process result + if markForDelete && forceDelete { + return fmt.Errorf("timeout of %s reached, the cluster was successfully marked for deletion but was not removed in time", timeout) + } + return fmt.Errorf("timeout of %s reached, the cluster was not marked for deletion, please try again", timeout) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_cluster.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_cluster.tmpl new file mode 100644 index 000000000..82aef730c --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_cluster.tmpl @@ -0,0 +1,163 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + cluster-role.tkg.tanzu.vmware.com/management: "" + tanzuKubernetesRelease: "{{.TkrVersion}}" + tkg.tanzu.vmware.com/cluster-name: "{{.ClusterName}}" + annotations: + osInfo: "ubuntu,20.04,amd64" + TKGVERSION: "{{.TkgVersion}}" +spec: + clusterNetwork: + pods: + cidrBlocks: + - "{{.PodCidr}}" + serviceDomain: cluster.local + services: + cidrBlocks: + - "{{.ServiceCidr}}" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDCluster + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDCluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +spec: + site: "{{.VcdSite}}" + org: "{{.Org}}" + ovdc: "{{.OrgVdc}}" + ovdcNetwork: "{{.OrgVdcNetwork}}" + {{- if .ControlPlaneEndpoint}} + controlPlaneEndpoint: + host: "{{.ControlPlaneEndpoint}}" + port: 6443 + {{- end}} + {{- if .VirtualIpSubnet}} + loadBalancerConfigSpec: + vipSubnet: "{{.VirtualIpSubnet}}" + {{- end}} + useAsManagementCluster: false + userContext: + secretRef: + name: capi-user-credentials + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.ControlPlaneSizingPolicy}}" + placementPolicy: "{{.ControlPlanePlacementPolicy}}" + storageProfile: "{{.ControlPlaneStorageProfile}}" + diskSize: {{.ControlPlaneDiskSize}} +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + kubeadmConfigSpec: + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + clusterConfiguration: + apiServer: + certSANs: + - localhost + - 127.0.0.1 + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + dns: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.DnsVersion}}" + etcd: + local: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.EtcdVersion}}" + imageRepository: "{{.ContainerRegistryUrl}}" + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + initConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + machineTemplate: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + replicas: {{.ControlPlaneMachineCount}} + version: "{{.KubernetesVersion}}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + useExperimentalRetryJoin: true + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_mhc.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_mhc.tmpl new file mode 100644 index 000000000..5d6d912ba --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_mhc.tmpl @@ -0,0 +1,22 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineHealthCheck +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + clusterctl.cluster.x-k8s.io: "" + clusterctl.cluster.x-k8s.io/move: "" +spec: + clusterName: "{{.ClusterName}}" + maxUnhealthy: "{{.MaxUnhealthyNodePercentage}}" + nodeStartupTimeout: "{{.NodeStartupTimeout}}" + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: "{{.ClusterName}}" + unhealthyConditions: + - type: Ready + status: Unknown + timeout: "{{.NodeUnknownTimeout}}" + - type: Ready + status: "False" + timeout: "{{.NodeNotReadyTimeout}}" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_workerpool.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_workerpool.tmpl new file mode 100644 index 000000000..9b7ffbe0c --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/capiyaml_workerpool.tmpl @@ -0,0 +1,41 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.NodePoolSizingPolicy}}" + placementPolicy: "{{.NodePoolPlacementPolicy}}" + storageProfile: "{{.NodePoolStorageProfile}}" + diskSize: "{{.NodePoolDiskSize}}" + enableNvidiaGPU: {{.NodePoolEnableGpu}} +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + clusterName: "{{.ClusterName}}" + replicas: {{.NodePoolMachineCount}} + selector: + matchLabels: null + template: + spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" + clusterName: "{{.ClusterName}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + version: "{{.KubernetesVersion}}" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/rde.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/rde.tmpl new file mode 100644 index 000000000..e5ea3e2b8 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.1/rde.tmpl @@ -0,0 +1,31 @@ +{ + "apiVersion": "capvcd.vmware.com/v1.1", + "kind": "CAPVCDCluster", + "name": "{{.Name}}", + "metadata": { + "name": "{{.Name}}", + "orgName": "{{.Org}}", + "site": "{{.VcdUrl}}", + "virtualDataCenterName": "{{.Vdc}}" + }, + "spec": { + "vcdKe": { + "isVCDKECluster": true, + "markForDelete": {{.Delete}}, + "forceDelete": {{.ForceDelete}}, + "autoRepairOnErrors": {{.AutoRepairOnErrors}}, + {{- if .DefaultStorageClassName }} + "defaultStorageClassOptions": { + "filesystem": "{{.DefaultStorageClassFileSystem}}", + "k8sStorageClassName": "{{.DefaultStorageClassName}}", + "vcdStorageProfileName": "{{.DefaultStorageClassStorageProfile}}", + "useDeleteReclaimPolicy": {{.DefaultStorageClassUseDeleteReclaimPolicy}} + }, + {{- end }} + "secure": { + "apiToken": "{{.ApiToken}}" + } + }, + "capiYaml": "{{.CapiYaml}}" + } +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_cluster.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_cluster.tmpl new file mode 100644 index 000000000..82aef730c --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_cluster.tmpl @@ -0,0 +1,163 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + cluster-role.tkg.tanzu.vmware.com/management: "" + tanzuKubernetesRelease: "{{.TkrVersion}}" + tkg.tanzu.vmware.com/cluster-name: "{{.ClusterName}}" + annotations: + osInfo: "ubuntu,20.04,amd64" + TKGVERSION: "{{.TkgVersion}}" +spec: + clusterNetwork: + pods: + cidrBlocks: + - "{{.PodCidr}}" + serviceDomain: cluster.local + services: + cidrBlocks: + - "{{.ServiceCidr}}" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDCluster + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDCluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +spec: + site: "{{.VcdSite}}" + org: "{{.Org}}" + ovdc: "{{.OrgVdc}}" + ovdcNetwork: "{{.OrgVdcNetwork}}" + {{- if .ControlPlaneEndpoint}} + controlPlaneEndpoint: + host: "{{.ControlPlaneEndpoint}}" + port: 6443 + {{- end}} + {{- if .VirtualIpSubnet}} + loadBalancerConfigSpec: + vipSubnet: "{{.VirtualIpSubnet}}" + {{- end}} + useAsManagementCluster: false + userContext: + secretRef: + name: capi-user-credentials + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.ControlPlaneSizingPolicy}}" + placementPolicy: "{{.ControlPlanePlacementPolicy}}" + storageProfile: "{{.ControlPlaneStorageProfile}}" + diskSize: {{.ControlPlaneDiskSize}} +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + kubeadmConfigSpec: + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + clusterConfiguration: + apiServer: + certSANs: + - localhost + - 127.0.0.1 + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + dns: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.DnsVersion}}" + etcd: + local: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.EtcdVersion}}" + imageRepository: "{{.ContainerRegistryUrl}}" + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + initConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + machineTemplate: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + replicas: {{.ControlPlaneMachineCount}} + version: "{{.KubernetesVersion}}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + useExperimentalRetryJoin: true + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_mhc.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_mhc.tmpl new file mode 100644 index 000000000..5d6d912ba --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_mhc.tmpl @@ -0,0 +1,22 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineHealthCheck +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + clusterctl.cluster.x-k8s.io: "" + clusterctl.cluster.x-k8s.io/move: "" +spec: + clusterName: "{{.ClusterName}}" + maxUnhealthy: "{{.MaxUnhealthyNodePercentage}}" + nodeStartupTimeout: "{{.NodeStartupTimeout}}" + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: "{{.ClusterName}}" + unhealthyConditions: + - type: Ready + status: Unknown + timeout: "{{.NodeUnknownTimeout}}" + - type: Ready + status: "False" + timeout: "{{.NodeNotReadyTimeout}}" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_workerpool.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_workerpool.tmpl new file mode 100644 index 000000000..9b7ffbe0c --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/capiyaml_workerpool.tmpl @@ -0,0 +1,41 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.NodePoolSizingPolicy}}" + placementPolicy: "{{.NodePoolPlacementPolicy}}" + storageProfile: "{{.NodePoolStorageProfile}}" + diskSize: "{{.NodePoolDiskSize}}" + enableNvidiaGPU: {{.NodePoolEnableGpu}} +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + clusterName: "{{.ClusterName}}" + replicas: {{.NodePoolMachineCount}} + selector: + matchLabels: null + template: + spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" + clusterName: "{{.ClusterName}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + version: "{{.KubernetesVersion}}" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/rde.tmpl b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/rde.tmpl new file mode 100644 index 000000000..7eb0011b3 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/4.2/rde.tmpl @@ -0,0 +1,32 @@ +{ + "apiVersion": "capvcd.vmware.com/v1.1", + "kind": "CAPVCDCluster", + "name": "{{.Name}}", + "metadata": { + "name": "{{.Name}}", + "orgName": "{{.Org}}", + "site": "{{.VcdUrl}}", + "virtualDataCenterName": "{{.Vdc}}" + }, + "spec": { + "vcdKe": { + "isVCDKECluster": true, + "markForDelete": {{.Delete}}, + "forceDelete": {{.ForceDelete}}, + "autoRepairOnErrors": {{.AutoRepairOnErrors}}, + {{- if .DefaultStorageClassName }} + "defaultStorageClassOptions": { + "filesystem": "{{.DefaultStorageClassFileSystem}}", + "k8sStorageClassName": "{{.DefaultStorageClassName}}", + "vcdStorageProfileName": "{{.DefaultStorageClassStorageProfile}}", + "useDeleteReclaimPolicy": {{.DefaultStorageClassUseDeleteReclaimPolicy}} + }, + {{- end }} + "secure": { + "apiToken": "{{.ApiToken}}" + } + }, + "capiYaml": "{{.CapiYaml}}", + "projector": { "operations": [] } + } +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/tkg_versions.json b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/tkg_versions.json new file mode 100644 index 000000000..ea8ca2fa2 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse/tkg_versions.json @@ -0,0 +1,128 @@ +{ + "v1.28.4+vmware.1-tkg.1-1e7baa840b8869c8bdce0cafff0da59d": { + "tkg": "v2.5.0", + "tkr": "v1.28.4---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.10.1_vmware.13" + }, + "v1.27.8+vmware.1-tkg.1-e77cdad8d69e4f76f2ded5e1356235b3": { + "tkg": "v2.5.0", + "tkr": "v1.27.8---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.10.1_vmware.12" + }, + "v1.27.5+vmware.1-tkg.1-0eb96d2f9f4f705ac87c40633d4b69st": { + "tkg": "v2.4.0", + "tkr": "v1.27.5---vmware.1-tkg.1", + "etcd": "v3.5.7_vmware.6", + "coreDns": "v1.10.1_vmware.7" + }, + "v1.26.11+vmware.1-tkg.1-6d29b7d826cdaa3535e156392e8d18cc": { + "tkg": "v2.5.0", + "tkr": "v1.26.11---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.9.3_vmware.19" + }, + "v1.26.8+vmware.1-tkg.1-b8c57a6c8c98d227f74e7b1a9eef27st": { + "tkg": "v2.4.0", + "tkr": "v1.26.8---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.26.8+vmware.1-tkg.1-0edd4dafbefbdb503f64d5472e500cf8": { + "tkg": "v2.3.1", + "tkr": "v1.26.8---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.25.13+vmware.1-tkg.1-0031669997707d1c644156b8fc31ebst": { + "tkg": "v2.4.0", + "tkr": "v1.25.13---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.25.13+vmware.1-tkg.1-6f7650434fd3787d751e8fb3c9e2153d": { + "tkg": "v2.3.1", + "tkr": "v1.25.13---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.11" + }, + "v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc": { + "tkg": "v2.2.0", + "tkr": "v1.25.7---vmware.2-tkg.1", + "etcd": "v3.5.6_vmware.9", + "coreDns": "v1.9.3_vmware.8" + }, + "v1.24.17+vmware.1-tkg.1-9f70d901a7d851fb115411e6790fdeae": { + "tkg": "v2.3.1", + "tkr": "v1.24.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.19", + "coreDns": "v1.8.6_vmware.26" + }, + "v1.24.11+vmware.1-tkg.1-2ccb2a001f8bd8f15f1bfbc811071830": { + "tkg": "v2.2.0", + "tkr": "v1.24.11---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.10", + "coreDns": "v1.8.6_vmware.18" + }, + "v1.24.10+vmware.1-tkg.1-765d418b72c247c2310384e640ee075e": { + "tkg": "v2.1.1", + "tkr": "v1.24.10---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.6", + "coreDns": "v1.8.6_vmware.17" + }, + "v1.23.17+vmware.1-tkg.1-ee4d95d5d08cd7f31da47d1480571754": { + "tkg": "v2.2.0", + "tkr": "v1.23.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.11", + "coreDns": "v1.8.6_vmware.19" + }, + "v1.23.16+vmware.1-tkg.1-eb0de9755338b944ea9652e6f758b3ce": { + "tkg": "v2.1.1", + "tkr": "v1.23.16---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.5", + "coreDns": "v1.8.6_vmware.16" + }, + "v1.22.17+vmware.1-tkg.1-df08b304658a6cf17f5e74dc0ab7543c": { + "tkg": "v2.1.1", + "tkr": "v1.22.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.1", + "coreDns": "v1.8.4_vmware.10" + }, + "v1.22.9+vmware.1-tkg.1-2182cbabee08edf480ee9bc5866d6933": { + "tkg": "v1.5.4", + "tkr": "v1.22.9---vmware.1-tkg.1", + "etcd": "v3.5.4_vmware.2", + "coreDns": "v1.8.4_vmware.9" + }, + "v1.21.11+vmware.1-tkg.2-d788dbbb335710c0a0d1a28670057896": { + "tkg": "v1.5.4", + "tkr": "v1.21.11---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.27", + "coreDns": "v1.8.0_vmware.13" + }, + "v1.21.8+vmware.1-tkg.2-ed3c93616a02968be452fe1934a1d37c": { + "tkg": "v1.4.3", + "tkr": "v1.21.8---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.25", + "coreDns": "v1.8.0_vmware.11" + }, + "v1.20.15+vmware.1-tkg.2-839faf7d1fa7fa356be22b72170ce1a8": { + "tkg": "v1.5.4", + "tkr": "v1.20.15---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.23", + "coreDns": "v1.7.0_vmware.15" + }, + "v1.20.14+vmware.1-tkg.2-5a5027ce2528a6229acb35b38ff8084e": { + "tkg": "v1.4.3", + "tkr": "v1.20.14---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.23", + "coreDns": "v1.7.0_vmware.15" + }, + "v1.19.16+vmware.1-tkg.2-fba68db15591c15fcd5f26b512663a42": { + "tkg": "v1.4.3", + "tkr": "v1.19.16---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.19", + "coreDns": "v1.7.0_vmware.15" + } +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_internal.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_internal.go new file mode 100644 index 000000000..9546ff245 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_internal.go @@ -0,0 +1,238 @@ +package govcd + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "strconv" + "strings" + "text/template" +) + +// This collection of files contains all the Go Templates and resources required for the Container Service Extension (CSE) methods +// to work. +// +//go:embed cse +var cseFiles embed.FS + +// getUnmarshalledRdePayload gets the unmarshalled JSON payload to create the Runtime Defined Entity that represents +// a CSE Kubernetes cluster, by using the receiver information. This method uses all the Go Templates stored in cseFiles +func (clusterSettings *cseClusterSettingsInternal) getUnmarshalledRdePayload() (map[string]interface{}, error) { + if clusterSettings == nil { + return nil, fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + capiYaml, err := clusterSettings.generateCapiYamlAsJsonString() + if err != nil { + return nil, err + } + + templateArgs := map[string]string{ + "Name": clusterSettings.Name, + "Org": clusterSettings.OrganizationName, + "VcdUrl": clusterSettings.VcdUrl, + "Vdc": clusterSettings.VdcName, + "Delete": "false", + "ForceDelete": "false", + "AutoRepairOnErrors": strconv.FormatBool(clusterSettings.AutoRepairOnErrors), + "ApiToken": clusterSettings.ApiToken, + "CapiYaml": capiYaml, + } + + if clusterSettings.DefaultStorageClass.StorageProfileName != "" { + templateArgs["DefaultStorageClassStorageProfile"] = clusterSettings.DefaultStorageClass.StorageProfileName + templateArgs["DefaultStorageClassName"] = clusterSettings.DefaultStorageClass.Name + templateArgs["DefaultStorageClassUseDeleteReclaimPolicy"] = strconv.FormatBool(clusterSettings.DefaultStorageClass.UseDeleteReclaimPolicy) + templateArgs["DefaultStorageClassFileSystem"] = clusterSettings.DefaultStorageClass.Filesystem + } + + rdeTemplate, err := getCseTemplate(clusterSettings.CseVersion, "rde") + if err != nil { + return nil, err + } + + rdePayload := template.Must(template.New(clusterSettings.Name).Parse(rdeTemplate)) + buf := &bytes.Buffer{} + if err := rdePayload.Execute(buf, templateArgs); err != nil { + return nil, fmt.Errorf("could not render the Go template with the RDE JSON: %s", err) + } + + var result interface{} + err = json.Unmarshal(buf.Bytes(), &result) + if err != nil { + return nil, fmt.Errorf("could not generate a correct RDE payload: %s", err) + } + + return result.(map[string]interface{}), nil +} + +// generateCapiYamlAsJsonString generates the "capiYaml" property of the RDE that represents a Kubernetes cluster. This +// "capiYaml" property is a YAML encoded as a JSON string. This method uses the Go Templates stored in cseFiles. +func (clusterSettings *cseClusterSettingsInternal) generateCapiYamlAsJsonString() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver cluster settings is nil") + } + + capiYamlTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_cluster") + if err != nil { + return "", err + } + + // This YAML snippet contains special strings, such as "%,", that render wrong using the Go template engine + sanitizedCapiYamlTemplate := strings.NewReplacer("%", "%%").Replace(capiYamlTemplate) + capiYaml := template.Must(template.New(clusterSettings.Name + "-cluster").Parse(sanitizedCapiYamlTemplate)) + + nodePoolYaml, err := clusterSettings.generateWorkerPoolsYaml() + if err != nil { + return "", err + } + + memoryHealthCheckYaml, err := clusterSettings.generateMachineHealthCheckYaml() + if err != nil { + return "", err + } + + templateArgs := map[string]interface{}{ + "ClusterName": clusterSettings.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + "TkrVersion": clusterSettings.TkgVersionBundle.TkrVersion, + "TkgVersion": clusterSettings.TkgVersionBundle.TkgVersion, + "PodCidr": clusterSettings.PodCidr, + "ServiceCidr": clusterSettings.ServiceCidr, + "VcdSite": clusterSettings.VcdUrl, + "Org": clusterSettings.OrganizationName, + "OrgVdc": clusterSettings.VdcName, + "OrgVdcNetwork": clusterSettings.NetworkName, + "Catalog": clusterSettings.CatalogName, + "VAppTemplate": clusterSettings.KubernetesTemplateOvaName, + "ControlPlaneSizingPolicy": clusterSettings.ControlPlane.SizingPolicyName, + "ControlPlanePlacementPolicy": clusterSettings.ControlPlane.PlacementPolicyName, + "ControlPlaneStorageProfile": clusterSettings.ControlPlane.StorageProfileName, + "ControlPlaneDiskSize": fmt.Sprintf("%dGi", clusterSettings.ControlPlane.DiskSizeGi), + "ControlPlaneMachineCount": strconv.Itoa(clusterSettings.ControlPlane.MachineCount), + "ControlPlaneEndpoint": clusterSettings.ControlPlane.Ip, + "DnsVersion": clusterSettings.TkgVersionBundle.CoreDnsVersion, + "EtcdVersion": clusterSettings.TkgVersionBundle.EtcdVersion, + "ContainerRegistryUrl": clusterSettings.VcdKeConfig.ContainerRegistryUrl, + "KubernetesVersion": clusterSettings.TkgVersionBundle.KubernetesVersion, + "SshPublicKey": clusterSettings.SshPublicKey, + "VirtualIpSubnet": clusterSettings.VirtualIpSubnet, + "Base64Certificates": clusterSettings.VcdKeConfig.Base64Certificates, + } + + buf := &bytes.Buffer{} + if err := capiYaml.Execute(buf, templateArgs); err != nil { + return "", fmt.Errorf("could not generate a correct CAPI YAML: %s", err) + } + + // The final "pretty" YAML. To embed it in the final payload it must be marshaled into a one-line JSON string + prettyYaml := "" + if memoryHealthCheckYaml != "" { + prettyYaml += fmt.Sprintf("%s\n---\n", memoryHealthCheckYaml) + } + prettyYaml += fmt.Sprintf("%s\n---\n%s", nodePoolYaml, buf.String()) + + // We don't use a standard json.Marshal() as the YAML contains special characters that are not encoded properly, such as '<'. + buf.Reset() + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err = enc.Encode(prettyYaml) + if err != nil { + return "", fmt.Errorf("could not encode the CAPI YAML into a JSON string: %s", err) + } + + // Removes trailing quotes from the final JSON string + return strings.Trim(strings.TrimSpace(buf.String()), "\""), nil +} + +// generateWorkerPoolsYaml generates YAML blocks corresponding to the cluster Worker Pools. The blocks are separated by +// the standard YAML separator (---), but does not add one at the end. +func (clusterSettings *cseClusterSettingsInternal) generateWorkerPoolsYaml() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + + workerPoolsTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_workerpool") + if err != nil { + return "", err + } + + workerPools := template.Must(template.New(clusterSettings.Name + "-worker-pool").Parse(workerPoolsTemplate)) + resultYaml := "" + buf := &bytes.Buffer{} + + // We can have many Worker Pools, we build a YAML object for each one of them. + for i, wp := range clusterSettings.WorkerPools { + + // Check the correctness of the Compute Policies in the node pool block + if wp.PlacementPolicyName != "" && wp.VGpuPolicyName != "" { + return "", fmt.Errorf("the Worker Pool '%s' should have either a Placement Policy or a vGPU Policy, not both", wp.Name) + } + placementPolicy := wp.PlacementPolicyName + if wp.VGpuPolicyName != "" { + // For convenience, we just use one of the variables as both cannot be set at same time + placementPolicy = wp.VGpuPolicyName + } + + if err := workerPools.Execute(buf, map[string]string{ + "ClusterName": clusterSettings.Name, + "NodePoolName": wp.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + "Catalog": clusterSettings.CatalogName, + "VAppTemplate": clusterSettings.KubernetesTemplateOvaName, + "NodePoolSizingPolicy": wp.SizingPolicyName, + "NodePoolPlacementPolicy": placementPolicy, // Can be either Placement or vGPU policy + "NodePoolStorageProfile": wp.StorageProfileName, + "NodePoolDiskSize": fmt.Sprintf("%dGi", wp.DiskSizeGi), + "NodePoolEnableGpu": strconv.FormatBool(wp.VGpuPolicyName != ""), + "NodePoolMachineCount": strconv.Itoa(wp.MachineCount), + "KubernetesVersion": clusterSettings.TkgVersionBundle.KubernetesVersion, + }); err != nil { + return "", fmt.Errorf("could not generate a correct Worker Pool '%s' YAML block: %s", wp.Name, err) + } + resultYaml += fmt.Sprintf("%s\n", buf.String()) + if i < len(clusterSettings.WorkerPools)-1 { + resultYaml += "---\n" + } + buf.Reset() + } + return resultYaml, nil +} + +// generateMachineHealthCheckYaml generates a YAML block corresponding to the cluster Machine Health Check. +// The generated YAML does not contain a separator (---) at the end. +func (clusterSettings *cseClusterSettingsInternal) generateMachineHealthCheckYaml() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + + if clusterSettings.VcdKeConfig.NodeStartupTimeout == "" && + clusterSettings.VcdKeConfig.NodeUnknownTimeout == "" && + clusterSettings.VcdKeConfig.NodeNotReadyTimeout == "" && + clusterSettings.VcdKeConfig.MaxUnhealthyNodesPercentage == 0 { + return "", nil + } + + mhcTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_mhc") + if err != nil { + return "", err + } + + machineHealthCheck := template.Must(template.New(clusterSettings.Name + "-mhc").Parse(mhcTemplate)) + buf := &bytes.Buffer{} + + if err := machineHealthCheck.Execute(buf, map[string]string{ + "ClusterName": clusterSettings.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + // With the 'percentage' suffix + "MaxUnhealthyNodePercentage": fmt.Sprintf("%.0f%%", clusterSettings.VcdKeConfig.MaxUnhealthyNodesPercentage), + // These values coming from VCDKEConfig (CSE Server settings) may have an "s" suffix. We make sure we don't duplicate it + "NodeStartupTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeStartupTimeout, "s", "")), + "NodeUnknownTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeUnknownTimeout, "s", "")), + "NodeNotReadyTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeNotReadyTimeout, "s", "")), + }); err != nil { + return "", fmt.Errorf("could not generate a correct Machine Health Check YAML: %s", err) + } + return fmt.Sprintf("%s\n", buf.String()), nil + +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_type.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_type.go new file mode 100644 index 000000000..90ca308b6 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_type.go @@ -0,0 +1,203 @@ +package govcd + +import ( + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "time" +) + +// CseKubernetesCluster is a type for managing an existing Kubernetes cluster created by the Container Service Extension (CSE) +type CseKubernetesCluster struct { + CseClusterSettings + ID string + Etag string + KubernetesVersion semver.Version + TkgVersion semver.Version + CapvcdVersion semver.Version + ClusterResourceSetBindings []string + CpiVersion semver.Version + CsiVersion semver.Version + State string + Events []CseClusterEvent + + client *Client + capvcdType *types.Capvcd + supportedUpgrades []*types.VAppTemplate // Caches the vApp templates that can be used to upgrade a cluster. +} + +// CseClusterSettings defines the required configuration of a Container Service Extension (CSE) Kubernetes cluster. +type CseClusterSettings struct { + CseVersion semver.Version + Name string + OrganizationId string + VdcId string + NetworkId string + KubernetesTemplateOvaId string + ControlPlane CseControlPlaneSettings + WorkerPools []CseWorkerPoolSettings + DefaultStorageClass *CseDefaultStorageClassSettings // Optional + Owner string // Optional, if not set will pick the current session user from the VCDClient + ApiToken string + NodeHealthCheck bool + PodCidr string + ServiceCidr string + SshPublicKey string + VirtualIpSubnet string + AutoRepairOnErrors bool +} + +// CseControlPlaneSettings defines the required configuration of a Control Plane of a Container Service Extension (CSE) Kubernetes cluster. +type CseControlPlaneSettings struct { + MachineCount int + DiskSizeGi int + SizingPolicyId string // Optional + PlacementPolicyId string // Optional + StorageProfileId string // Optional + Ip string // Optional +} + +// CseWorkerPoolSettings defines the required configuration of a Worker Pool of a Container Service Extension (CSE) Kubernetes cluster. +type CseWorkerPoolSettings struct { + Name string + MachineCount int + DiskSizeGi int + SizingPolicyId string // Optional + PlacementPolicyId string // Optional + VGpuPolicyId string // Optional + StorageProfileId string // Optional +} + +// CseDefaultStorageClassSettings defines the required configuration of a Default Storage Class of a Container Service Extension (CSE) Kubernetes cluster. +type CseDefaultStorageClassSettings struct { + StorageProfileId string + Name string + ReclaimPolicy string // Must be either "delete" or "retain" + Filesystem string // Must be either "ext4" or "xfs" +} + +// CseClusterEvent is an event that has occurred during the lifetime of a Container Service Extension (CSE) Kubernetes cluster. +type CseClusterEvent struct { + Name string + Type string + ResourceId string + ResourceName string + OccurredAt time.Time + Details string +} + +// CseClusterUpdateInput defines the required configuration that a Container Service Extension (CSE) Kubernetes cluster needs in order to be updated. +type CseClusterUpdateInput struct { + KubernetesTemplateOvaId *string + ControlPlane *CseControlPlaneUpdateInput + WorkerPools *map[string]CseWorkerPoolUpdateInput // Maps a node pool name with its contents + NewWorkerPools *[]CseWorkerPoolSettings + NodeHealthCheck *bool + AutoRepairOnErrors *bool +} + +// CseControlPlaneUpdateInput defines the required configuration that the Control Plane of the Container Service Extension (CSE) Kubernetes cluster +// needs in order to be updated. +type CseControlPlaneUpdateInput struct { + MachineCount int +} + +// CseWorkerPoolUpdateInput defines the required configuration that a Worker Pool of the Container Service Extension (CSE) Kubernetes cluster +// needs in order to be updated. +type CseWorkerPoolUpdateInput struct { + MachineCount int +} + +// cseClusterSettingsInternal defines the required arguments that are required by the CSE Server used internally to specify +// a Kubernetes cluster. These are not set by the user, but instead they are computed from a valid +// CseClusterSettings object in the CseClusterSettings.toCseClusterSettingsInternal method. These fields are then +// inserted in Go templates to render a final JSON that is valid to be used as the cluster Runtime Defined Entity (RDE) payload. +// +// The main difference between CseClusterSettings and this structure is that the first one uses IDs and this one uses names, among +// other differences like the computed tkgVersionBundle. +type cseClusterSettingsInternal struct { + CseVersion semver.Version + Name string + OrganizationName string + VdcName string + NetworkName string + KubernetesTemplateOvaName string + TkgVersionBundle tkgVersionBundle + CatalogName string + RdeType *types.DefinedEntityType + ControlPlane cseControlPlaneSettingsInternal + WorkerPools []cseWorkerPoolSettingsInternal + DefaultStorageClass cseDefaultStorageClassInternal + VcdKeConfig vcdKeConfig + Owner string + ApiToken string + VcdUrl string + VirtualIpSubnet string + SshPublicKey string + PodCidr string + ServiceCidr string + AutoRepairOnErrors bool +} + +// tkgVersionBundle is a type that contains all the versions of the components of +// a Kubernetes cluster that can be obtained with the internal properties of the Kubernetes Template OVAs downloaded from +// https://customerconnect.vmware.com +type tkgVersionBundle struct { + EtcdVersion string + CoreDnsVersion string + TkgVersion string + TkrVersion string + KubernetesVersion string +} + +// cseControlPlaneSettingsInternal defines the Control Plane inside cseClusterSettingsInternal +type cseControlPlaneSettingsInternal struct { + MachineCount int + DiskSizeGi int + SizingPolicyName string + PlacementPolicyName string + StorageProfileName string + Ip string +} + +// cseWorkerPoolSettingsInternal defines a Worker Pool inside cseClusterSettingsInternal +type cseWorkerPoolSettingsInternal struct { + Name string + MachineCount int + DiskSizeGi int + SizingPolicyName string + PlacementPolicyName string + VGpuPolicyName string + StorageProfileName string +} + +// cseDefaultStorageClassInternal defines a Default Storage Class inside cseClusterSettingsInternal +type cseDefaultStorageClassInternal struct { + StorageProfileName string + Name string + UseDeleteReclaimPolicy bool + Filesystem string +} + +// vcdKeConfig is a type that contains only the required and relevant fields from the VCDKEConfig (CSE Server) configuration, +// such as the Machine Health Check settings. +type vcdKeConfig struct { + MaxUnhealthyNodesPercentage float64 + NodeStartupTimeout string + NodeNotReadyTimeout string + NodeUnknownTimeout string + ContainerRegistryUrl string + Base64Certificates []string +} + +// cseComponentsVersions is a type that registers the versions of the subcomponents of a specific CSE Version +type cseComponentsVersions struct { + VcdKeConfigRdeTypeVersion string + CapvcdRdeTypeVersion string + CseInterfaceVersion string +} + +// Constants that define the RDE Type of a CSE Kubernetes cluster +const ( + cseKubernetesClusterVendor = "vmware" + cseKubernetesClusterNamespace = "capvcdCluster" +) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_util.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_util.go new file mode 100644 index 000000000..d323f6ba1 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_util.go @@ -0,0 +1,981 @@ +package govcd + +import ( + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "net" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// getCseComponentsVersions gets the versions of the subcomponents that are part of Container Service Extension. +// NOTE: This function should be updated on every CSE release to update the supported versions. +func getCseComponentsVersions(cseVersion semver.Version) (*cseComponentsVersions, error) { + v43, _ := semver.NewVersion("4.3.0") + v42, _ := semver.NewVersion("4.2.0") + v41, _ := semver.NewVersion("4.1.0") + err := fmt.Errorf("the Container Service Extension version '%s' is not supported", cseVersion.String()) + + if cseVersion.GreaterThanOrEqual(v43) { + return nil, err + } + if cseVersion.GreaterThanOrEqual(v42) { + return &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.3.0", + CseInterfaceVersion: "1.0.0", + }, nil + } + if cseVersion.GreaterThanOrEqual(v41) { + return &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.2.0", + CseInterfaceVersion: "1.0.0", + }, nil + } + return nil, err +} + +// cseConvertToCseKubernetesClusterType takes a generic RDE that must represent an existing CSE Kubernetes cluster, +// and transforms it to an equivalent CseKubernetesCluster object that represents the same cluster, but +// it is easy to explore and consume. If the input RDE is not a CSE Kubernetes cluster, this method +// will obviously return an error. +// +// The transformation from a generic RDE to a CseKubernetesCluster is done by querying VCD for every needed item, +// such as Network IDs, Compute Policies IDs, vApp Template IDs, etc. It deeply explores the RDE contents +// (even the CAPI YAML) to retrieve information and getting the missing pieces from VCD. +// +// WARNING: Don't use this method inside loops or avoid calling it multiple times in a row, as it performs many queries +// to VCD. +func cseConvertToCseKubernetesClusterType(rde *DefinedEntity) (*CseKubernetesCluster, error) { + requiredType := fmt.Sprintf("%s:%s", cseKubernetesClusterVendor, cseKubernetesClusterNamespace) + + if !strings.Contains(rde.DefinedEntity.ID, requiredType) || !strings.Contains(rde.DefinedEntity.EntityType, requiredType) { + return nil, fmt.Errorf("the receiver RDE is not a '%s' entity, it is '%s'", requiredType, rde.DefinedEntity.EntityType) + } + + entityBytes, err := json.Marshal(rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("could not marshal the RDE contents to create a capvcdType instance: %s", err) + } + + capvcd := &types.Capvcd{} + err = json.Unmarshal(entityBytes, &capvcd) + if err != nil { + return nil, fmt.Errorf("could not unmarshal the RDE contents to create a Capvcd instance: %s", err) + } + + result := &CseKubernetesCluster{ + CseClusterSettings: CseClusterSettings{ + Name: rde.DefinedEntity.Name, + ApiToken: "******", // We must not return this one, we return the "standard" 6-asterisk value + AutoRepairOnErrors: capvcd.Spec.VcdKe.AutoRepairOnErrors, + ControlPlane: CseControlPlaneSettings{}, + }, + ID: rde.DefinedEntity.ID, + Etag: rde.Etag, + ClusterResourceSetBindings: make([]string, len(capvcd.Status.Capvcd.ClusterResourceSetBindings)), + State: capvcd.Status.VcdKe.State, + Events: make([]CseClusterEvent, 0), + client: rde.client, + capvcdType: capvcd, + supportedUpgrades: make([]*types.VAppTemplate, 0), + } + + // Add all events to the resulting cluster + for _, s := range capvcd.Status.VcdKe.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedEvent, + }) + } + for _, s := range capvcd.Status.VcdKe.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Capvcd.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Capvcd.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Cpi.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Cpi.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Csi.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Csi.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Projector.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Projector.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + sort.SliceStable(result.Events, func(i, j int) bool { + return result.Events[i].OccurredAt.After(result.Events[j].OccurredAt) + }) + + if capvcd.Status.Capvcd.CapvcdVersion != "" { + version, err := semver.NewVersion(capvcd.Status.Capvcd.CapvcdVersion) + if err != nil { + return nil, fmt.Errorf("could not read Capvcd version: %s", err) + } + result.CapvcdVersion = *version + } + + if capvcd.Status.Cpi.Version != "" { + version, err := semver.NewVersion(strings.TrimSpace(capvcd.Status.Cpi.Version)) // Note: We use trim as the version comes with spacing characters + if err != nil { + return nil, fmt.Errorf("could not read CPI version: %s", err) + } + result.CpiVersion = *version + } + + if capvcd.Status.Csi.Version != "" { + version, err := semver.NewVersion(capvcd.Status.Csi.Version) + if err != nil { + return nil, fmt.Errorf("could not read CSI version: %s", err) + } + result.CsiVersion = *version + } + + if capvcd.Status.VcdKe.VcdKeVersion != "" { + cseVersion, err := semver.NewVersion(capvcd.Status.VcdKe.VcdKeVersion) + if err != nil { + return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err) + } + // Remove the possible version suffixes as we just want MAJOR.MINOR.PATCH + // TODO: This can be replaced with (*cseVersion).Core() in newer versions of the library + cseVersionSegs := (*cseVersion).Segments() + cseVersion, err = semver.NewVersion(fmt.Sprintf("%d.%d.%d", cseVersionSegs[0], cseVersionSegs[1], cseVersionSegs[2])) + if err != nil { + return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err) + } + result.CseVersion = *cseVersion + } + + // Retrieve the Owner + if rde.DefinedEntity.Owner != nil { + result.Owner = rde.DefinedEntity.Owner.Name + } + + // Retrieve the Organization ID + for i, binding := range capvcd.Status.Capvcd.ClusterResourceSetBindings { + result.ClusterResourceSetBindings[i] = binding.Name + } + + if len(capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints) > 0 { + result.ControlPlane.Ip = capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints[0].Host + } + + if len(result.capvcdType.Status.Capvcd.VcdProperties.Organizations) > 0 { + result.OrganizationId = result.capvcdType.Status.Capvcd.VcdProperties.Organizations[0].Id + } + + // If the Org/VDC information is not set, we can't continue retrieving information for the cluster. + // This scenario is when the cluster is not correctly provisioned (Error state) + if len(result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs) == 0 { + return result, nil + } + + // NOTE: The code below, until the end of this function, requires the Org/VDC information + + // Retrieve the VDC ID + result.VdcId = result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Id + // FIXME: This is a workaround, because for some reason the OrgVdcs[*].Id property contains the VDC name instead of the VDC ID. + // Once this is fixed, this conditional should not be needed anymore. + if result.VdcId == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name { + vdcs, err := queryOrgVdcList(rde.client, map[string]string{}) + if err != nil { + return nil, fmt.Errorf("could not get VDC IDs as no VDC was found: %s", err) + } + found := false + for _, vdc := range vdcs { + if vdc.Name == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name { + result.VdcId = fmt.Sprintf("urn:vcloud:vdc:%s", extractUuid(vdc.HREF)) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("could not get VDC IDs as no VDC with name '%s' was found", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name) + } + } + + // Retrieve the Network ID + params := url.Values{} + params.Add("filter", fmt.Sprintf("name==%s", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].OvdcNetworkName)) + params = queryParameterFilterAnd("ownerRef.id=="+result.VdcId, params) + networks, err := getAllOpenApiOrgVdcNetworks(rde.client, params) + if err != nil { + return nil, fmt.Errorf("could not read Org VDC Network from Capvcd type: %s", err) + } + if len(networks) != 1 { + return nil, fmt.Errorf("expected one Org VDC Network from Capvcd type, but got %d", len(networks)) + } + result.NetworkId = networks[0].OpenApiOrgVdcNetwork.ID + + // Here we retrieve several items that we need from now onwards, like Storage Profiles and Compute Policies + storageProfiles := map[string]string{} + if rde.client.IsSysAdmin { + allSp, err := queryAdminOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId) + if err != nil { + return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err) + } + for _, recordType := range allSp { + storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF)) + } + } else { + allSp, err := queryOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId) + if err != nil { + return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err) + } + for _, recordType := range allSp { + storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF)) + } + } + + computePolicies, err := getAllVdcComputePoliciesV2(rde.client, nil) + if err != nil { + return nil, fmt.Errorf("could not get all the Compute Policies: %s", err) + } + + if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName != "" { // This would mean there is a Default Storage Class defined + result.DefaultStorageClass = &CseDefaultStorageClassSettings{ + Name: result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName, + ReclaimPolicy: "retain", + Filesystem: result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.Filesystem, + } + for spName, spId := range storageProfiles { + if spName == result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.VcdStorageProfileName { + result.DefaultStorageClass.StorageProfileId = spId + } + } + if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.UseDeleteReclaimPolicy { + result.DefaultStorageClass.ReclaimPolicy = "delete" + } + } + + // NOTE: We get the remaining elements from the CAPI YAML, despite they are also inside capvcdType.Status. + // The reason is that any change on the cluster is immediately reflected in the CAPI YAML, but not in the capvcdType.Status + // elements, which may take more than 10 minutes to be refreshed. + yamlDocuments, err := unmarshalMultipleYamlDocuments(result.capvcdType.Spec.CapiYaml) + if err != nil { + return nil, err + } + + // We need a map of worker pools and not a slice, because there are two types of YAML documents + // that contain data about a specific worker pool (VCDMachineTemplate and MachineDeployment), and we can get them in no + // particular order, so we store the worker pools with their name as key. This way we can easily (O(1)) fetch and update them. + workerPools := map[string]CseWorkerPoolSettings{} + for _, yamlDocument := range yamlDocuments { + switch yamlDocument["kind"] { + case "KubeadmControlPlane": + result.ControlPlane.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas")) + users := traverseMapAndGet[[]interface{}](yamlDocument, "spec.kubeadmConfigSpec.users") + if len(users) == 0 { + return nil, fmt.Errorf("expected 'spec.kubeadmConfigSpec.users' slice to not to be empty") + } + keys := traverseMapAndGet[[]interface{}](users[0], "sshAuthorizedKeys") + if len(keys) > 0 { + result.SshPublicKey = keys[0].(string) // Optional field + } + + version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "spec.version")) + if err != nil { + return nil, fmt.Errorf("could not read Kubernetes version: %s", err) + } + result.KubernetesVersion = *version + + case "VCDMachineTemplate": + name := traverseMapAndGet[string](yamlDocument, "metadata.name") + sizingPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.sizingPolicy") + placementPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.placementPolicy") + storageProfileName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.storageProfile") + diskSizeGi, err := strconv.Atoi(strings.ReplaceAll(traverseMapAndGet[string](yamlDocument, "spec.template.spec.diskSize"), "Gi", "")) + if err != nil { + return nil, err + } + + if strings.Contains(name, "control-plane-node-pool") { + // This is the single Control Plane + for _, policy := range computePolicies { + if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly { + result.ControlPlane.SizingPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly { + result.ControlPlane.PlacementPolicyId = policy.VdcComputePolicyV2.ID + } + } + for spName, spId := range storageProfiles { + if storageProfileName == spName { + result.ControlPlane.StorageProfileId = spId + } + } + + result.ControlPlane.DiskSizeGi = diskSizeGi + + // We retrieve the Kubernetes Template OVA just once for the Control Plane because all YAML blocks share the same + vAppTemplateName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.template") + catalogName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.catalog") + vAppTemplates, err := queryVappTemplateListWithFilter(rde.client, map[string]string{ + "catalogName": catalogName, + "name": vAppTemplateName, + }) + if err != nil { + return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s': %s", vAppTemplateName, catalogName, err) + } + if len(vAppTemplates) == 0 { + return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s'", vAppTemplateName, catalogName) + } + // The records don't have ID set, so we calculate it + result.KubernetesTemplateOvaId = fmt.Sprintf("urn:vcloud:vapptemplate:%s", extractUuid(vAppTemplates[0].HREF)) + } else { + // This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the + // machine count from MachineDeployment. + if _, ok := workerPools[name]; !ok { + workerPools[name] = CseWorkerPoolSettings{} + } + workerPool := workerPools[name] + workerPool.Name = name + for _, policy := range computePolicies { + if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly { + workerPool.SizingPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && !policy.VdcComputePolicyV2.IsVgpuPolicy { + workerPool.PlacementPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && policy.VdcComputePolicyV2.IsVgpuPolicy { + workerPool.VGpuPolicyId = policy.VdcComputePolicyV2.ID + } + } + for spName, spId := range storageProfiles { + if storageProfileName == spName { + workerPool.StorageProfileId = spId + } + } + workerPool.DiskSizeGi = diskSizeGi + workerPools[name] = workerPool // Override the worker pool with the updated data + } + case "MachineDeployment": + name := traverseMapAndGet[string](yamlDocument, "metadata.name") + // This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the + // other information from VCDMachineTemplate. + if _, ok := workerPools[name]; !ok { + workerPools[name] = CseWorkerPoolSettings{} + } + workerPool := workerPools[name] + workerPool.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas")) + workerPools[name] = workerPool // Override the worker pool with the updated data + case "VCDCluster": + result.VirtualIpSubnet = traverseMapAndGet[string](yamlDocument, "spec.loadBalancerConfigSpec.vipSubnet") + case "Cluster": + version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "metadata.annotations.TKGVERSION")) + if err != nil { + return nil, fmt.Errorf("could not read TKG version: %s", err) + } + result.TkgVersion = *version + + cidrBlocks := traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.pods.cidrBlocks") + if len(cidrBlocks) == 0 { + return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.pods.cidrBlocks' item") + } + result.PodCidr = cidrBlocks[0].(string) + + cidrBlocks = traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.services.cidrBlocks") + if len(cidrBlocks) == 0 { + return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.services.cidrBlocks' item") + } + result.ServiceCidr = cidrBlocks[0].(string) + case "MachineHealthCheck": + // This is quite simple, if we find this document, means that Machine Health Check is enabled + result.NodeHealthCheck = true + } + } + result.WorkerPools = make([]CseWorkerPoolSettings, len(workerPools)) + i := 0 + for _, workerPool := range workerPools { + result.WorkerPools[i] = workerPool + i++ + } + + return result, nil +} + +// waitUntilClusterIsProvisioned waits for the Kubernetes cluster to be in "provisioned" state, either indefinitely (if timeout = 0) +// or until the timeout is reached. +// If one of the states of the cluster at a given point is "error", this function also checks whether the cluster has the "AutoRepairOnErrors" flag enabled, +// so it keeps waiting if it's true. +// If timeout is reached before the cluster is in "provisioned" state, it returns an error. +func waitUntilClusterIsProvisioned(client *Client, clusterId string, timeout time.Duration) error { + var elapsed time.Duration + sleepTime := 10 + + start := time.Now() + capvcd := &types.Capvcd{} + for elapsed <= timeout || timeout == 0 { // If the user specifies timeout=0, we wait forever + rde, err := getRdeById(client, clusterId) + if err != nil { + return err + } + + // Here we don't use cseConvertToCseKubernetesClusterType to avoid calling VCD. We only need the state. + entityBytes, err := json.Marshal(rde.DefinedEntity.Entity) + if err != nil { + return fmt.Errorf("could not check the Kubernetes cluster state: %s", err) + } + err = json.Unmarshal(entityBytes, &capvcd) + if err != nil { + return fmt.Errorf("could not check the Kubernetes cluster state: %s", err) + } + + switch capvcd.Status.VcdKe.State { + case "provisioned": + return nil + case "error": + // We just finish if auto-recovery is disabled, otherwise we just let CSE fixing things in background + if !capvcd.Spec.VcdKe.AutoRepairOnErrors { + // Give feedback about what went wrong + errors := "" + for _, event := range capvcd.Status.Capvcd.ErrorSet { + errors += fmt.Sprintf("%s,\n", event.AdditionalDetails.DetailedError) + } + return fmt.Errorf("got an error and 'AutoRepairOnErrors' is disabled, aborting. Error events:\n%s", errors) + } + } + + util.Logger.Printf("[DEBUG] Cluster '%s' is in '%s' state, will check again in %d seconds", rde.DefinedEntity.ID, capvcd.Status.VcdKe.State, sleepTime) + elapsed = time.Since(start) + time.Sleep(time.Duration(sleepTime) * time.Second) + } + return fmt.Errorf("timeout of %s reached, latest cluster state obtained was '%s'", timeout, capvcd.Status.VcdKe.State) +} + +// validate validates the receiver CseClusterSettings. Returns an error if any of the fields is empty or wrong. +func (input *CseClusterSettings) validate() error { + if input == nil { + return fmt.Errorf("the receiver CseClusterSettings cannot be nil") + } + // This regular expression is used to validate the constraints placed by Container Service Extension on the names + // of the components of the Kubernetes clusters: + // Names must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters. + cseNamesRegex, err := regexp.Compile(`^[a-z](?:[a-z0-9-]{0,29}[a-z0-9])?$`) + if err != nil { + return fmt.Errorf("could not compile regular expression '%s'", err) + } + + _, err = getCseComponentsVersions(input.CseVersion) + if err != nil { + return err + } + if !cseNamesRegex.MatchString(input.Name) { + return fmt.Errorf("the name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.Name) + } + if input.OrganizationId == "" { + return fmt.Errorf("the Organization ID is required") + } + if input.VdcId == "" { + return fmt.Errorf("the VDC ID is required") + } + if input.NetworkId == "" { + return fmt.Errorf("the Network ID is required") + } + if input.KubernetesTemplateOvaId == "" { + return fmt.Errorf("the Kubernetes Template OVA ID is required") + } + if input.ControlPlane.MachineCount < 1 || input.ControlPlane.MachineCount%2 == 0 { + return fmt.Errorf("number of Control Plane nodes must be odd and higher than 0, but it was '%d'", input.ControlPlane.MachineCount) + } + if input.ControlPlane.DiskSizeGi < 20 { + return fmt.Errorf("disk size for the Control Plane in Gibibytes (Gi) must be at least 20, but it was '%d'", input.ControlPlane.DiskSizeGi) + } + if len(input.WorkerPools) == 0 { + return fmt.Errorf("there must be at least one Worker Pool") + } + existingWorkerPools := map[string]bool{} + for _, workerPool := range input.WorkerPools { + if _, alreadyExists := existingWorkerPools[workerPool.Name]; alreadyExists { + return fmt.Errorf("the names of the Worker Pools must be unique, but '%s' is repeated", workerPool.Name) + } + if workerPool.MachineCount < 1 { + return fmt.Errorf("number of Worker Pool '%s' nodes must higher than 0, but it was '%d'", workerPool.Name, workerPool.MachineCount) + } + if workerPool.DiskSizeGi < 20 { + return fmt.Errorf("disk size for the Worker Pool '%s' in Gibibytes (Gi) must be at least 20, but it was '%d'", workerPool.Name, workerPool.DiskSizeGi) + } + if !cseNamesRegex.MatchString(workerPool.Name) { + return fmt.Errorf("the Worker Pool name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", workerPool.Name) + } + existingWorkerPools[workerPool.Name] = true + } + if input.DefaultStorageClass != nil { // This field is optional + if !cseNamesRegex.MatchString(input.DefaultStorageClass.Name) { + return fmt.Errorf("the Default Storage Class name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.DefaultStorageClass.Name) + } + if input.DefaultStorageClass.StorageProfileId == "" { + return fmt.Errorf("the Storage Profile ID for the Default Storage Class is required") + } + if input.DefaultStorageClass.ReclaimPolicy != "delete" && input.DefaultStorageClass.ReclaimPolicy != "retain" { + return fmt.Errorf("the Reclaim Policy for the Default Storage Class must be either 'delete' or 'retain', but it was '%s'", input.DefaultStorageClass.ReclaimPolicy) + } + if input.DefaultStorageClass.Filesystem != "ext4" && input.DefaultStorageClass.ReclaimPolicy != "xfs" { + return fmt.Errorf("the filesystem for the Default Storage Class must be either 'ext4' or 'xfs', but it was '%s'", input.DefaultStorageClass.Filesystem) + } + } + if input.ApiToken == "" { + return fmt.Errorf("the API token is required") + } + if input.PodCidr == "" { + return fmt.Errorf("the Pod CIDR is required") + } + if _, _, err := net.ParseCIDR(input.PodCidr); err != nil { + return fmt.Errorf("the Pod CIDR is malformed: %s", err) + } + if input.ServiceCidr == "" { + return fmt.Errorf("the Service CIDR is required") + } + if _, _, err := net.ParseCIDR(input.ServiceCidr); err != nil { + return fmt.Errorf("the Service CIDR is malformed: %s", err) + } + if input.VirtualIpSubnet != "" { + if _, _, err := net.ParseCIDR(input.VirtualIpSubnet); err != nil { + return fmt.Errorf("the Virtual IP Subnet is malformed: %s", err) + } + } + if input.ControlPlane.Ip != "" { + if r := net.ParseIP(input.ControlPlane.Ip); r == nil { + return fmt.Errorf("the Control Plane IP is malformed: %s", input.ControlPlane.Ip) + } + } + return nil +} + +// toCseClusterSettingsInternal transforms user input data (CseClusterSettings) into the final payload that +// will be used to define a Container Service Extension Kubernetes cluster (cseClusterSettingsInternal). +// +// For example, the most relevant transformation is the change of the item IDs that are present in CseClusterSettings +// (such as CseClusterSettings.KubernetesTemplateOvaId) to their corresponding Names (e.g. cseClusterSettingsInternal.KubernetesTemplateOvaName), +// which are the identifiers that Container Service Extension uses internally. +func (input *CseClusterSettings) toCseClusterSettingsInternal(org Org) (*cseClusterSettingsInternal, error) { + err := input.validate() + if err != nil { + return nil, err + } + + output := &cseClusterSettingsInternal{} + if org.Org == nil { + return nil, fmt.Errorf("could not retrieve the Organization, it is nil") + } + output.OrganizationName = org.Org.Name + + vdc, err := org.GetVDCById(input.VdcId, true) + if err != nil { + return nil, fmt.Errorf("could not retrieve the VDC with ID '%s': %s", input.VdcId, err) + } + output.VdcName = vdc.Vdc.Name + + vAppTemplate, err := getVAppTemplateById(org.client, input.KubernetesTemplateOvaId) + if err != nil { + return nil, fmt.Errorf("could not retrieve the Kubernetes Template OVA with ID '%s': %s", input.KubernetesTemplateOvaId, err) + } + output.KubernetesTemplateOvaName = vAppTemplate.VAppTemplate.Name + + tkgVersions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + return nil, fmt.Errorf("could not retrieve the required information from the Kubernetes Template OVA: %s", err) + } + output.TkgVersionBundle = tkgVersions + + catalogName, err := vAppTemplate.GetCatalogName() + if err != nil { + return nil, fmt.Errorf("could not retrieve the Catalog name where the the Kubernetes Template OVA '%s' (%s) is hosted: %s", input.KubernetesTemplateOvaId, vAppTemplate.VAppTemplate.Name, err) + } + output.CatalogName = catalogName + + network, err := vdc.GetOrgVdcNetworkById(input.NetworkId, true) + if err != nil { + return nil, fmt.Errorf("could not retrieve the Org VDC Network with ID '%s': %s", input.NetworkId, err) + } + output.NetworkName = network.OrgVDCNetwork.Name + + cseComponentsVersions, err := getCseComponentsVersions(input.CseVersion) + if err != nil { + return nil, err + } + rdeType, err := getRdeType(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseComponentsVersions.CapvcdRdeTypeVersion) + if err != nil { + return nil, err + } + output.RdeType = rdeType.DefinedEntityType + + // Gather all the IDs of the Compute Policies and Storage Profiles, so we can transform them to Names in bulk. + var computePolicyIds []string + var storageProfileIds []string + for _, w := range input.WorkerPools { + computePolicyIds = append(computePolicyIds, w.SizingPolicyId, w.PlacementPolicyId, w.VGpuPolicyId) + storageProfileIds = append(storageProfileIds, w.StorageProfileId) + } + computePolicyIds = append(computePolicyIds, input.ControlPlane.SizingPolicyId, input.ControlPlane.PlacementPolicyId) + storageProfileIds = append(storageProfileIds, input.ControlPlane.StorageProfileId) + if input.DefaultStorageClass != nil { + storageProfileIds = append(storageProfileIds, input.DefaultStorageClass.StorageProfileId) + } + + idToNameCache, err := idToNames(org.client, computePolicyIds, storageProfileIds) + if err != nil { + return nil, err + } + + // Now that everything is cached in memory, we can build the Node pools and Storage Class payloads in a trivial way. + output.WorkerPools = make([]cseWorkerPoolSettingsInternal, len(input.WorkerPools)) + for i, w := range input.WorkerPools { + output.WorkerPools[i] = cseWorkerPoolSettingsInternal{ + Name: w.Name, + MachineCount: w.MachineCount, + DiskSizeGi: w.DiskSizeGi, + } + output.WorkerPools[i].SizingPolicyName = idToNameCache[w.SizingPolicyId] + output.WorkerPools[i].PlacementPolicyName = idToNameCache[w.PlacementPolicyId] + output.WorkerPools[i].VGpuPolicyName = idToNameCache[w.VGpuPolicyId] + output.WorkerPools[i].StorageProfileName = idToNameCache[w.StorageProfileId] + } + output.ControlPlane = cseControlPlaneSettingsInternal{ + MachineCount: input.ControlPlane.MachineCount, + DiskSizeGi: input.ControlPlane.DiskSizeGi, + SizingPolicyName: idToNameCache[input.ControlPlane.SizingPolicyId], + PlacementPolicyName: idToNameCache[input.ControlPlane.PlacementPolicyId], + StorageProfileName: idToNameCache[input.ControlPlane.StorageProfileId], + Ip: input.ControlPlane.Ip, + } + + if input.DefaultStorageClass != nil { + output.DefaultStorageClass = cseDefaultStorageClassInternal{ + StorageProfileName: idToNameCache[input.DefaultStorageClass.StorageProfileId], + Name: input.DefaultStorageClass.Name, + Filesystem: input.DefaultStorageClass.Filesystem, + } + output.DefaultStorageClass.UseDeleteReclaimPolicy = false + if input.DefaultStorageClass.ReclaimPolicy == "delete" { + output.DefaultStorageClass.UseDeleteReclaimPolicy = true + } + } + + vcdKeConfig, err := getVcdKeConfig(org.client, cseComponentsVersions.VcdKeConfigRdeTypeVersion, input.NodeHealthCheck) + if err != nil { + return nil, err + } + output.VcdKeConfig = vcdKeConfig + + output.Owner = input.Owner + if input.Owner == "" { + sessionInfo, err := org.client.GetSessionInfo() + if err != nil { + return nil, fmt.Errorf("error getting the Owner: %s", err) + } + output.Owner = sessionInfo.User.Name + } + + output.VcdUrl = strings.Replace(org.client.VCDHREF.String(), "/api", "", 1) + + // These don't change, don't need mapping + output.ApiToken = input.ApiToken + output.AutoRepairOnErrors = input.AutoRepairOnErrors + output.CseVersion = input.CseVersion + output.Name = input.Name + output.PodCidr = input.PodCidr + output.ServiceCidr = input.ServiceCidr + output.SshPublicKey = input.SshPublicKey + output.VirtualIpSubnet = input.VirtualIpSubnet + + return output, nil +} + +// getTkgVersionBundleFromVAppTemplate returns a tkgVersionBundle with the details of +// all the Kubernetes cluster components versions given a valid Kubernetes Template OVA. +// If it is not a valid Kubernetes Template OVA, returns an error. +func getTkgVersionBundleFromVAppTemplate(template *types.VAppTemplate) (tkgVersionBundle, error) { + result := tkgVersionBundle{} + if template == nil { + return result, fmt.Errorf("the Kubernetes Template OVA is nil") + } + if template.Children == nil || len(template.Children.VM) == 0 { + return result, fmt.Errorf("the Kubernetes Template OVA '%s' doesn't have any child VM", template.Name) + } + if template.Children.VM[0].ProductSection == nil { + return result, fmt.Errorf("the Product section of the Kubernetes Template OVA '%s' is empty, can't proceed", template.Name) + } + id := "" + for _, prop := range template.Children.VM[0].ProductSection.Property { + if prop != nil && prop.Key == "VERSION" { + id = prop.DefaultValue // Use DefaultValue and not Value as the value we want is in the "value" attr + } + } + if id == "" { + return result, fmt.Errorf("could not find any VERSION property inside the Kubernetes Template OVA '%s' Product section", template.Name) + } + + tkgVersionsMap := "cse/tkg_versions.json" + cseTkgVersionsJson, err := cseFiles.ReadFile(tkgVersionsMap) + if err != nil { + return result, fmt.Errorf("failed reading %s: %s", tkgVersionsMap, err) + } + + versionsMap := map[string]interface{}{} + err = json.Unmarshal(cseTkgVersionsJson, &versionsMap) + if err != nil { + return result, fmt.Errorf("failed unmarshalling %s: %s", tkgVersionsMap, err) + } + versionMap, ok := versionsMap[id] + if !ok { + return result, fmt.Errorf("the Kubernetes Template OVA '%s' is not supported", template.Name) + } + + // We don't need to check the Split result because the map checking above guarantees that the ID is well-formed. + idParts := strings.Split(id, "-") + result.KubernetesVersion = idParts[0] + result.TkrVersion = versionMap.(map[string]interface{})["tkr"].(string) + result.TkgVersion = versionMap.(map[string]interface{})["tkg"].(string) + result.EtcdVersion = versionMap.(map[string]interface{})["etcd"].(string) + result.CoreDnsVersion = versionMap.(map[string]interface{})["coreDns"].(string) + return result, nil +} + +// compareTkgVersion returns -1, 0 or 1 if the receiver TKG version is less than, equal or higher to the input TKG version. +// If they cannot be compared it returns -2. +func (tkgVersions tkgVersionBundle) compareTkgVersion(tkgVersion string) int { + receiverVersion, err := semver.NewVersion(tkgVersions.TkgVersion) + if err != nil { + return -2 + } + inputVersion, err := semver.NewVersion(tkgVersion) + if err != nil { + return -2 + } + return receiverVersion.Compare(inputVersion) +} + +// kubernetesVersionIsUpgradeableFrom returns true either if the receiver Kubernetes version is exactly one minor version higher +// than the given input version (being the minor digit the 'Y' in 'X.Y.Z') or if the minor is the same, but the patch is higher +// (being the minor digit the 'Z' in 'X.Y.Z'). +// Any malformed version returns false. +// Examples: +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = true +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.2") = false +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.0") = true +// * "1.19.10".kubernetesVersionIsUpgradeableFrom("1.18.0") = true +// * "1.20.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +// * "1.21.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +// * "1.18.0".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +func (tkgVersions tkgVersionBundle) kubernetesVersionIsUpgradeableFrom(kubernetesVersion string) bool { + upgradeToVersion, err := semver.NewVersion(tkgVersions.KubernetesVersion) + if err != nil { + return false + } + fromVersion, err := semver.NewVersion(kubernetesVersion) + if err != nil { + return false + } + + if upgradeToVersion.Equal(fromVersion) { + return false + } + + upgradeToVersionSegments := upgradeToVersion.Segments() + if len(upgradeToVersionSegments) < 2 { + return false + } + fromVersionSegments := fromVersion.Segments() + if len(fromVersionSegments) < 2 { + return false + } + + majorIsEqual := upgradeToVersionSegments[0] == fromVersionSegments[0] + minorIsJustOneHigher := upgradeToVersionSegments[1]-1 == fromVersionSegments[1] + minorIsEqual := upgradeToVersionSegments[1] == fromVersionSegments[1] + patchIsHigher := upgradeToVersionSegments[2] > fromVersionSegments[2] + + return majorIsEqual && (minorIsJustOneHigher || (minorIsEqual && patchIsHigher)) +} + +// getVcdKeConfig gets the required information from the CSE Server configuration RDE (VCDKEConfig), such as the +// Machine Health Check settings and the Container Registry URL. +func getVcdKeConfig(client *Client, vcdKeConfigVersion string, retrieveMachineHealtchCheckInfo bool) (vcdKeConfig, error) { + result := vcdKeConfig{} + rdes, err := getRdesByName(client, "vmware", "VCDKEConfig", vcdKeConfigVersion, "vcdKeConfig") + if err != nil { + return result, err + } + if len(rdes) != 1 { + return result, fmt.Errorf("expected exactly one VCDKEConfig RDE with version '%s', but got %d", vcdKeConfigVersion, len(rdes)) + } + + profiles, ok := rdes[0].DefinedEntity.Entity["profiles"].([]interface{}) + if !ok { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'profiles' array") + } + if len(profiles) == 0 { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a non-empty 'profiles' element") + } + + // We append /tkg as required, even in air-gapped environments: + // https://docs.vmware.com/en/VMware-Cloud-Director-Container-Service-Extension/4.2/VMware-Cloud-Director-Container-Service-Extension-Install-provider-4.2/GUID-B5C19221-2ECA-4DCD-8EA1-8E391F6217C1.html + result.ContainerRegistryUrl = fmt.Sprintf("%s/tkg", profiles[0].(map[string]interface{})["containerRegistryUrl"]) + + k8sConfig, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{}) + if !ok { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'K8Config' object") + } + certificates, ok := k8sConfig["certificateAuthorities"] + if ok { + result.Base64Certificates = make([]string, len(certificates.([]interface{}))) + for i, certificate := range certificates.([]interface{}) { + result.Base64Certificates[i] = base64.StdEncoding.EncodeToString([]byte(certificate.(string))) + } + } + + if retrieveMachineHealtchCheckInfo { + mhc, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{})["mhc"] + if !ok { + // If there is no "mhc" entry in the VCDKEConfig JSON, we skip setting this part of the Kubernetes cluster configuration + return result, nil + } + result.MaxUnhealthyNodesPercentage = mhc.(map[string]interface{})["maxUnhealthyNodes"].(float64) + result.NodeStartupTimeout = mhc.(map[string]interface{})["nodeStartupTimeout"].(string) + result.NodeNotReadyTimeout = mhc.(map[string]interface{})["nodeUnknownTimeout"].(string) + result.NodeUnknownTimeout = mhc.(map[string]interface{})["nodeNotReadyTimeout"].(string) + } + + return result, nil +} + +// idToNames returns a map that associates Compute Policies/Storage Profiles IDs with their respective names. +// This is useful as the input to create/update a cluster uses different entities IDs, but CSE cluster creation/update process uses Names. +// For that reason, we need to transform IDs to Names by querying VCD +func idToNames(client *Client, computePolicyIds, storageProfileIds []string) (map[string]string, error) { + result := map[string]string{ + "": "", // Default empty value to map optional values that were not set, to avoid extra checks. For example, an empty vGPU Policy. + } + // Retrieve the Compute Policies and Storage Profiles names and put them in the resulting map. This map also can + // be used to reduce the calls to VCD. The URN format used by VCD guarantees that IDs are unique, so there is no possibility of clashes here. + for _, id := range storageProfileIds { + if _, alreadyPresent := result[id]; !alreadyPresent { + storageProfile, err := getStorageProfileById(client, id) + if err != nil { + return nil, fmt.Errorf("could not retrieve Storage Profile with ID '%s': %s", id, err) + } + result[id] = storageProfile.Name + } + } + for _, id := range computePolicyIds { + if _, alreadyPresent := result[id]; !alreadyPresent { + computePolicy, err := getVdcComputePolicyV2ById(client, id) + if err != nil { + return nil, fmt.Errorf("could not retrieve Compute Policy with ID '%s': %s", id, err) + } + result[id] = computePolicy.VdcComputePolicyV2.Name + } + } + return result, nil +} + +// getCseTemplate reads the Go template present in the embedded cseFiles filesystem. +func getCseTemplate(cseVersion semver.Version, templateName string) (string, error) { + minimumVersion, err := semver.NewVersion("4.1") + if err != nil { + return "", err + } + if cseVersion.LessThan(minimumVersion) { + return "", fmt.Errorf("the Container Service minimum version is '%s'", minimumVersion.String()) + } + versionSegments := cseVersion.Segments() + // We try with major.minor.patch + fullTemplatePath := fmt.Sprintf("cse/%d.%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], versionSegments[2], templateName) + result, err := cseFiles.ReadFile(fullTemplatePath) + if err != nil { + // We try now just with major.minor + fullTemplatePath = fmt.Sprintf("cse/%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], templateName) + result, err = cseFiles.ReadFile(fullTemplatePath) + if err != nil { + return "", fmt.Errorf("could not read Go template '%s.tmpl' for CSE version %s", templateName, cseVersion.String()) + } + } + return string(result), nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_yaml.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_yaml.go new file mode 100644 index 000000000..aa7e6b9ef --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/cse_yaml.go @@ -0,0 +1,411 @@ +package govcd + +import ( + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "sigs.k8s.io/yaml" + "strings" +) + +// updateCapiYaml takes a YAML and modifies its Kubernetes Template OVA, its Control plane, its Worker pools +// and its Node Health Check capabilities, by using the new values provided as input. +// If some of the values of the input is not provided, it doesn't change them. +// If none of the values is provided, it just returns the same untouched YAML. +func (cluster *CseKubernetesCluster) updateCapiYaml(input CseClusterUpdateInput) (string, error) { + if cluster == nil || cluster.capvcdType == nil { + return "", fmt.Errorf("receiver cluster is nil") + } + + if input.ControlPlane == nil && input.WorkerPools == nil && input.NodeHealthCheck == nil && input.KubernetesTemplateOvaId == nil && input.NewWorkerPools == nil { + return cluster.capvcdType.Spec.CapiYaml, nil + } + + // The YAML contains multiple documents, so we cannot use a simple yaml.Unmarshal() as this one just gets the first + // document it finds. + yamlDocs, err := unmarshalMultipleYamlDocuments(cluster.capvcdType.Spec.CapiYaml) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("error unmarshalling YAML: %s", err) + } + + if input.ControlPlane != nil { + err := cseUpdateControlPlaneInYaml(yamlDocs, *input.ControlPlane) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + if input.WorkerPools != nil { + err := cseUpdateWorkerPoolsInYaml(yamlDocs, *input.WorkerPools) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + // Order matters. We need to add the new pools before updating the Kubernetes template. + if input.NewWorkerPools != nil { + // Worker pool names must be unique + for _, existingPool := range cluster.WorkerPools { + for _, newPool := range *input.NewWorkerPools { + if newPool.Name == existingPool.Name { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("there is an existing Worker Pool with name '%s'", existingPool.Name) + } + } + } + + yamlDocs, err = cseAddWorkerPoolsInYaml(yamlDocs, *cluster, *input.NewWorkerPools) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + // As a side note, we can't optimize this one with "if equals do nothing" because + // in order to retrieve the current value we would need to explore the YAML anyway, which is what we also need to do to update it. + // Also, even if we did it, the current value obtained from YAML would be a Name, but the new value is an ID, so we would need to query VCD anyway + // as well. + // So in this special case this "optimization" would optimize nothing. The same happens with other YAML values. + if input.KubernetesTemplateOvaId != nil { + vAppTemplate, err := getVAppTemplateById(cluster.client, *input.KubernetesTemplateOvaId) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("could not retrieve the Kubernetes Template OVA with ID '%s': %s", *input.KubernetesTemplateOvaId, err) + } + // Check the versions of the selected OVA before upgrading + versions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("could not retrieve the TKG versions of OVA '%s': %s", *input.KubernetesTemplateOvaId, err) + } + if versions.compareTkgVersion(cluster.capvcdType.Status.Capvcd.Upgrade.Current.TkgVersion) < 0 || !versions.kubernetesVersionIsUpgradeableFrom(cluster.capvcdType.Status.Capvcd.Upgrade.Current.KubernetesVersion) { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("cannot perform an OVA change as the new one '%s' has an older TKG/Kubernetes version (%s/%s)", vAppTemplate.VAppTemplate.Name, versions.TkgVersion, versions.KubernetesVersion) + } + err = cseUpdateKubernetesTemplateInYaml(yamlDocs, vAppTemplate.VAppTemplate) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + if input.NodeHealthCheck != nil { + cseComponentsVersions, err := getCseComponentsVersions(cluster.CseVersion) + if err != nil { + return "", err + } + vcdKeConfig, err := getVcdKeConfig(cluster.client, cseComponentsVersions.VcdKeConfigRdeTypeVersion, *input.NodeHealthCheck) + if err != nil { + return "", err + } + yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, cluster.Name, cluster.CseVersion, vcdKeConfig) + if err != nil { + return "", err + } + } + + return marshalMultipleYamlDocuments(yamlDocs) +} + +// cseUpdateKubernetesTemplateInYaml modifies the given Kubernetes cluster YAML by modifying the Kubernetes Template OVA +// used by all the cluster elements. +// The caveat here is that not only VCDMachineTemplate needs to be changed with the new OVA name, but also +// other fields that reference the related Kubernetes version, TKG version and other derived information. +func cseUpdateKubernetesTemplateInYaml(yamlDocuments []map[string]interface{}, kubernetesTemplateOva *types.VAppTemplate) error { + tkgBundle, err := getTkgVersionBundleFromVAppTemplate(kubernetesTemplateOva) + if err != nil { + return err + } + for _, d := range yamlDocuments { + switch d["kind"] { + case "VCDMachineTemplate": + ok := traverseMapAndGet[string](d, "spec.template.spec.template") != "" + if !ok { + return fmt.Errorf("the VCDMachineTemplate 'spec.template.spec.template' field is missing") + } + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["template"] = kubernetesTemplateOva.Name + case "MachineDeployment": + ok := traverseMapAndGet[string](d, "spec.template.spec.version") != "" + if !ok { + return fmt.Errorf("the MachineDeployment 'spec.template.spec.version' field is missing") + } + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["version"] = tkgBundle.KubernetesVersion + case "Cluster": + ok := traverseMapAndGet[string](d, "metadata.annotations.TKGVERSION") != "" + if !ok { + return fmt.Errorf("the Cluster 'metadata.annotations.TKGVERSION' field is missing") + } + d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})["TKGVERSION"] = tkgBundle.TkgVersion + ok = traverseMapAndGet[string](d, "metadata.labels.tanzuKubernetesRelease") != "" + if !ok { + return fmt.Errorf("the Cluster 'metadata.labels.tanzuKubernetesRelease' field is missing") + } + d["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["tanzuKubernetesRelease"] = tkgBundle.TkrVersion + case "KubeadmControlPlane": + ok := traverseMapAndGet[string](d, "spec.version") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.version' field is missing") + } + d["spec"].(map[string]interface{})["version"] = tkgBundle.KubernetesVersion + ok = traverseMapAndGet[string](d, "spec.kubeadmConfigSpec.clusterConfiguration.dns.imageTag") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.kubeadmConfigSpec.clusterConfiguration.dns.imageTag' field is missing") + } + d["spec"].(map[string]interface{})["kubeadmConfigSpec"].(map[string]interface{})["clusterConfiguration"].(map[string]interface{})["dns"].(map[string]interface{})["imageTag"] = tkgBundle.CoreDnsVersion + ok = traverseMapAndGet[string](d, "spec.kubeadmConfigSpec.clusterConfiguration.etcd.local.imageTag") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.kubeadmConfigSpec.clusterConfiguration.etcd.local.imageTag' field is missing") + } + d["spec"].(map[string]interface{})["kubeadmConfigSpec"].(map[string]interface{})["clusterConfiguration"].(map[string]interface{})["etcd"].(map[string]interface{})["local"].(map[string]interface{})["imageTag"] = tkgBundle.EtcdVersion + } + } + return nil +} + +// cseUpdateControlPlaneInYaml modifies the given Kubernetes cluster YAML contents by changing the Control Plane with the input parameters. +func cseUpdateControlPlaneInYaml(yamlDocuments []map[string]interface{}, input CseControlPlaneUpdateInput) error { + if input.MachineCount < 1 || input.MachineCount%2 == 0 { + return fmt.Errorf("incorrect machine count for Control Plane: %d. Should be at least 1 and an odd number", input.MachineCount) + } + + updated := false + for _, d := range yamlDocuments { + if d["kind"] != "KubeadmControlPlane" { + continue + } + d["spec"].(map[string]interface{})["replicas"] = float64(input.MachineCount) // As it was originally unmarshalled as a float64 + updated = true + } + if !updated { + return fmt.Errorf("could not find the KubeadmControlPlane object in the YAML") + } + return nil +} + +// cseUpdateControlPlaneInYaml modifies the given Kubernetes cluster YAML contents by changing +// the existing Worker Pools with the input parameters. +func cseUpdateWorkerPoolsInYaml(yamlDocuments []map[string]interface{}, workerPools map[string]CseWorkerPoolUpdateInput) error { + updated := 0 + for _, d := range yamlDocuments { + if d["kind"] != "MachineDeployment" { + continue + } + + workerPoolName := traverseMapAndGet[string](d, "metadata.name") + if workerPoolName == "" { + return fmt.Errorf("the MachineDeployment 'metadata.name' field is empty") + } + + workerPoolToUpdate := "" + for wpName := range workerPools { + if wpName == workerPoolName { + workerPoolToUpdate = wpName + } + } + // This worker pool must not be updated as it is not present in the input, continue searching for the ones we want + if workerPoolToUpdate == "" { + continue + } + + if workerPools[workerPoolToUpdate].MachineCount < 0 { + return fmt.Errorf("incorrect machine count for worker pool %s: %d. Should be at least 0", workerPoolToUpdate, workerPools[workerPoolToUpdate].MachineCount) + } + + d["spec"].(map[string]interface{})["replicas"] = float64(workerPools[workerPoolToUpdate].MachineCount) // As it was originally unmarshalled as a float64 + updated++ + } + if updated != len(workerPools) { + return fmt.Errorf("could not update all the Node pools. Updated %d, expected %d", updated, len(workerPools)) + } + return nil +} + +// cseAddWorkerPoolsInYaml modifies the given Kubernetes cluster YAML contents by adding new Worker Pools +// described by the input parameters. +// NOTE: This function doesn't modify the input, but returns a copy of the YAML with the added unmarshalled documents. +func cseAddWorkerPoolsInYaml(docs []map[string]interface{}, cluster CseKubernetesCluster, newWorkerPools []CseWorkerPoolSettings) ([]map[string]interface{}, error) { + if len(newWorkerPools) == 0 { + return docs, nil + } + + var computePolicyIds []string + var storageProfileIds []string + for _, w := range newWorkerPools { + computePolicyIds = append(computePolicyIds, w.SizingPolicyId, w.PlacementPolicyId, w.VGpuPolicyId) + storageProfileIds = append(storageProfileIds, w.StorageProfileId) + } + + idToNameCache, err := idToNames(cluster.client, computePolicyIds, storageProfileIds) + if err != nil { + return nil, err + } + + internalSettings := cseClusterSettingsInternal{WorkerPools: make([]cseWorkerPoolSettingsInternal, len(newWorkerPools))} + for i, workerPool := range newWorkerPools { + internalSettings.WorkerPools[i] = cseWorkerPoolSettingsInternal{ + Name: workerPool.Name, + MachineCount: workerPool.MachineCount, + DiskSizeGi: workerPool.DiskSizeGi, + StorageProfileName: idToNameCache[workerPool.StorageProfileId], + SizingPolicyName: idToNameCache[workerPool.SizingPolicyId], + VGpuPolicyName: idToNameCache[workerPool.VGpuPolicyId], + PlacementPolicyName: idToNameCache[workerPool.PlacementPolicyId], + } + } + + // Extra information needed to render the YAML. As all the worker pools share the same + // Kubernetes OVA name, version and Catalog, we pick this info from any of the available ones. + for _, doc := range docs { + if internalSettings.CatalogName == "" && doc["kind"] == "VCDMachineTemplate" { + internalSettings.CatalogName = traverseMapAndGet[string](doc, "spec.template.spec.catalog") + } + if internalSettings.KubernetesTemplateOvaName == "" && doc["kind"] == "VCDMachineTemplate" { + internalSettings.KubernetesTemplateOvaName = traverseMapAndGet[string](doc, "spec.template.spec.template") + } + if internalSettings.TkgVersionBundle.KubernetesVersion == "" && doc["kind"] == "MachineDeployment" { + internalSettings.TkgVersionBundle.KubernetesVersion = traverseMapAndGet[string](doc, "spec.template.spec.version") + } + if internalSettings.CatalogName != "" && internalSettings.KubernetesTemplateOvaName != "" && internalSettings.TkgVersionBundle.KubernetesVersion != "" { + break + } + } + internalSettings.Name = cluster.Name + internalSettings.CseVersion = cluster.CseVersion + nodePoolsYaml, err := internalSettings.generateWorkerPoolsYaml() + if err != nil { + return nil, err + } + + newWorkerPoolsYamlDocs, err := unmarshalMultipleYamlDocuments(nodePoolsYaml) + if err != nil { + return nil, err + } + + result := make([]map[string]interface{}, len(docs)) + copy(result, docs) + return append(result, newWorkerPoolsYamlDocs...), nil +} + +// cseUpdateNodeHealthCheckInYaml updates the Kubernetes cluster described in the given YAML documents by adding or removing +// the MachineHealthCheck object. +// NOTE: This function doesn't modify the input, but returns a copy of the YAML with the modifications. +func cseUpdateNodeHealthCheckInYaml(yamlDocuments []map[string]interface{}, clusterName string, cseVersion semver.Version, vcdKeConfig vcdKeConfig) ([]map[string]interface{}, error) { + mhcPosition := -1 + result := make([]map[string]interface{}, len(yamlDocuments)) + for i, d := range yamlDocuments { + if d["kind"] == "MachineHealthCheck" { + mhcPosition = i + } + result[i] = d + } + + machineHealthCheckEnabled := vcdKeConfig.NodeUnknownTimeout != "" && vcdKeConfig.NodeStartupTimeout != "" && vcdKeConfig.NodeNotReadyTimeout != "" && + vcdKeConfig.MaxUnhealthyNodesPercentage != 0 + + if mhcPosition < 0 { + // There is no MachineHealthCheck block + if !machineHealthCheckEnabled { + // We don't want it neither, so nothing to do + return result, nil + } + + // We need to add the block to the slice of YAML documents + settings := &cseClusterSettingsInternal{CseVersion: cseVersion, Name: clusterName, VcdKeConfig: vcdKeConfig} + mhcYaml, err := settings.generateMachineHealthCheckYaml() + if err != nil { + return nil, err + } + var mhc map[string]interface{} + err = yaml.Unmarshal([]byte(mhcYaml), &mhc) + if err != nil { + return nil, err + } + result = append(result, mhc) + } else { + // There is a MachineHealthCheck block + if machineHealthCheckEnabled { + // We want it, but it is already there, so nothing to do + return result, nil + } + + // We don't want Machine Health Checks, we delete the YAML document + result[mhcPosition] = result[len(result)-1] // We override the MachineHealthCheck block with the last document + result = result[:len(result)-1] // We remove the last document (now duplicated) + } + return result, nil +} + +// marshalMultipleYamlDocuments takes a slice of maps representing multiple YAML documents (one per item in the slice) and +// marshals all of them into a single string with the corresponding separators "---". +func marshalMultipleYamlDocuments(yamlDocuments []map[string]interface{}) (string, error) { + result := "" + for i, yamlDoc := range yamlDocuments { + updatedSingleDoc, err := yaml.Marshal(yamlDoc) + if err != nil { + return "", fmt.Errorf("error marshaling the updated CAPVCD YAML '%v': %s", yamlDoc, err) + } + result += fmt.Sprintf("%s\n", updatedSingleDoc) + if i < len(yamlDocuments)-1 { // The last document doesn't need the YAML separator + result += "---\n" + } + } + return result, nil +} + +// unmarshalMultipleYamlDocuments takes a multi-document YAML (multiple YAML documents are separated by "---") and +// unmarshalls all of them into a slice of generic maps with the corresponding content. +func unmarshalMultipleYamlDocuments(yamlDocuments string) ([]map[string]interface{}, error) { + if len(strings.TrimSpace(yamlDocuments)) == 0 { + return []map[string]interface{}{}, nil + } + + splitYamlDocs := strings.Split(yamlDocuments, "---\n") + result := make([]map[string]interface{}, len(splitYamlDocs)) + for i, yamlDoc := range splitYamlDocs { + err := yaml.Unmarshal([]byte(yamlDoc), &result[i]) + if err != nil { + return nil, fmt.Errorf("could not unmarshal document %s: %s", yamlDoc, err) + } + } + + return result, nil +} + +// traverseMapAndGet traverses the input interface{}, which should be a map of maps, by following the path specified as +// "keyA.keyB.keyC.keyD", doing something similar to, visually speaking, map["keyA"]["keyB"]["keyC"]["keyD"], or in other words, +// it goes inside every inner map iteratively, until the given path is finished. +// If the path doesn't lead to any value, or if the value is nil, or there is any other issue, returns the "zero" value of T. +func traverseMapAndGet[T any](input interface{}, path string) T { + var nothing T + if input == nil { + return nothing + } + inputMap, ok := input.(map[string]interface{}) + if !ok { + return nothing + } + if len(inputMap) == 0 { + return nothing + } + pathUnits := strings.Split(path, ".") + completed := false + i := 0 + var result interface{} + for !completed { + subPath := pathUnits[i] + traversed, ok := inputMap[subPath] + if !ok { + return nothing + } + if i < len(pathUnits)-1 { + traversedMap, ok := traversed.(map[string]interface{}) + if !ok { + return nothing + } + inputMap = traversedMap + } else { + completed = true + result = traversed + } + i++ + } + resultTyped, ok := result.(T) + if !ok { + return nothing + } + return resultTyped +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_entity.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_entity.go index 5022cc1a0..2c8d74fa0 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_entity.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_entity.go @@ -9,7 +9,15 @@ import ( "fmt" "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" - "time" + "strings" +) + +const ( + labelDefinedEntity = "Defined Entity" + labelDefinedEntityType = "Defined Entity Type" + labelRdeBehavior = "RDE Behavior" + labelRdeBehaviorOverride = "RDE Behavior Override" + labelRdeBehaviorAccessControl = "RDE Behavior Access Control" ) // DefinedEntityType is a type for handling Runtime Defined Entity (RDE) Type definitions. @@ -19,6 +27,14 @@ type DefinedEntityType struct { client *Client } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (d DefinedEntityType) wrap(inner *types.DefinedEntityType) *DefinedEntityType { + d.DefinedEntityType = inner + return &d +} + // DefinedEntity represents an instance of a Runtime Defined Entity (RDE) type DefinedEntity struct { DefinedEntity *types.DefinedEntity @@ -26,72 +42,52 @@ type DefinedEntity struct { client *Client } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (d DefinedEntity) wrap(inner *types.DefinedEntity) *DefinedEntity { + d.DefinedEntity = inner + return &d +} + // CreateRdeType creates a Runtime Defined Entity Type. // Only a System administrator can create RDE Types. func (vcdClient *VCDClient) CreateRdeType(rde *types.DefinedEntityType) (*DefinedEntityType, error) { - client := vcdClient.Client - - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - result := &DefinedEntityType{ - DefinedEntityType: &types.DefinedEntityType{}, - client: &vcdClient.Client, - } - - err = client.OpenApiPostItem(apiVersion, urlRef, nil, rde, result.DefinedEntityType, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + entityLabel: labelDefinedEntityType, } - - return result, nil + outerType := DefinedEntityType{client: &vcdClient.Client} + return createOuterEntity(&vcdClient.Client, outerType, c, rde) } // GetAllRdeTypes retrieves all Runtime Defined Entity Types. Query parameters can be supplied to perform additional filtering. func (vcdClient *VCDClient) GetAllRdeTypes(queryParameters url.Values) ([]*DefinedEntityType, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - typeResponses := []*types.DefinedEntityType{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, err - } + return getAllRdeTypes(&vcdClient.Client, queryParameters) +} - // Wrap all typeResponses into DefinedEntityType types with client - returnRDEs := make([]*DefinedEntityType, len(typeResponses)) - for sliceIndex := range typeResponses { - returnRDEs[sliceIndex] = &DefinedEntityType{ - DefinedEntityType: typeResponses[sliceIndex], - client: &vcdClient.Client, - } +// getAllRdeTypes retrieves all Runtime Defined Entity Types. Query parameters can be supplied to perform additional filtering. +func getAllRdeTypes(client *Client, queryParameters url.Values) ([]*DefinedEntityType, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + entityLabel: labelDefinedEntityType, + queryParameters: queryParameters, } - return returnRDEs, nil + outerType := DefinedEntityType{client: client} + return getAllOuterEntities[DefinedEntityType, types.DefinedEntityType](client, outerType, c) } // GetRdeType gets a Runtime Defined Entity Type by its unique combination of vendor, nss and version. func (vcdClient *VCDClient) GetRdeType(vendor, nss, version string) (*DefinedEntityType, error) { + return getRdeType(&vcdClient.Client, vendor, nss, version) +} + +// getRdeType gets a Runtime Defined Entity Type by its unique combination of vendor, nss and version. +func getRdeType(client *Client, vendor, nss, version string) (*DefinedEntityType, error) { queryParameters := url.Values{} queryParameters.Add("filter", fmt.Sprintf("vendor==%s;nss==%s;version==%s", vendor, nss, version)) - rdeTypes, err := vcdClient.GetAllRdeTypes(queryParameters) + rdeTypes, err := getAllRdeTypes(client, queryParameters) if err != nil { return nil, err } @@ -109,35 +105,19 @@ func (vcdClient *VCDClient) GetRdeType(vendor, nss, version string) (*DefinedEnt // GetRdeTypeById gets a Runtime Defined Entity Type by its ID. func (vcdClient *VCDClient) GetRdeTypeById(id string) (*DefinedEntityType, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) - if err != nil { - return nil, err + c := crudConfig{ + entityLabel: labelDefinedEntityType, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{id}, } - result := &DefinedEntityType{ - DefinedEntityType: &types.DefinedEntityType{}, - client: &vcdClient.Client, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, result.DefinedEntityType, nil) - if err != nil { - return nil, err - } - - return result, nil + outerType := DefinedEntityType{client: &vcdClient.Client} + return getOuterEntity[DefinedEntityType, types.DefinedEntityType](&vcdClient.Client, outerType, c) } // Update updates the receiver Runtime Defined Entity Type with the values given by the input. // Only a System administrator can create RDE Types. func (rdeType *DefinedEntityType) Update(rdeTypeToUpdate types.DefinedEntityType) error { - client := rdeType.client if rdeType.DefinedEntityType.ID == "" { return fmt.Errorf("ID of the receiver Runtime Defined Entity Type is empty") } @@ -158,21 +138,19 @@ func (rdeType *DefinedEntityType) Update(rdeTypeToUpdate types.DefinedEntityType rdeTypeToUpdate.Nss = rdeType.DefinedEntityType.Nss rdeTypeToUpdate.Vendor = rdeType.DefinedEntityType.Vendor - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, rdeType.DefinedEntityType.ID) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{rdeType.DefinedEntityType.ID}, + entityLabel: labelDefinedEntityType, } - err = client.OpenApiPutItem(apiVersion, urlRef, nil, rdeTypeToUpdate, rdeType.DefinedEntityType, nil) + resultDefinedEntityType, err := updateInnerEntity(rdeType.client, c, &rdeTypeToUpdate) if err != nil { return err } + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + rdeType.DefinedEntityType = resultDefinedEntityType return nil } @@ -180,24 +158,13 @@ func (rdeType *DefinedEntityType) Update(rdeTypeToUpdate types.DefinedEntityType // Delete deletes the receiver Runtime Defined Entity Type. // Only a System administrator can delete RDE Types. func (rdeType *DefinedEntityType) Delete() error { - client := rdeType.client - if rdeType.DefinedEntityType.ID == "" { - return fmt.Errorf("ID of the receiver Runtime Defined Entity Type is empty") - } - - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, rdeType.DefinedEntityType.ID) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{rdeType.DefinedEntityType.ID}, + entityLabel: labelDefinedEntityType, } - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { + if err := deleteEntityById(rdeType.client, c); err != nil { return err } @@ -216,24 +183,12 @@ func (rdeType *DefinedEntityType) GetAllBehaviors(queryParameters url.Values) ([ // GetBehaviorById retrieves a unique Behavior that belongs to the receiver RDE Type and is determined by the // input ID. The ID can be a RDE Interface Behavior ID or a RDE Type overridden Behavior ID. func (rdeType *DefinedEntityType) GetBehaviorById(id string) (*types.Behavior, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors - apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), id) - if err != nil { - return nil, err - } - - response := types.Behavior{} - err = rdeType.client.OpenApiGetItem(apiVersion, urlRef, nil, &response, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, id}, + entityLabel: labelRdeBehavior, } - - return &response, nil + return getInnerEntity[types.Behavior](rdeType.client, c) } // GetBehaviorByName retrieves a unique Behavior that belongs to the receiver RDE Type and is named after @@ -243,12 +198,8 @@ func (rdeType *DefinedEntityType) GetBehaviorByName(name string) (*types.Behavio if err != nil { return nil, fmt.Errorf("could not get the Behaviors of the Defined Entity Type with ID '%s': %s", rdeType.DefinedEntityType.ID, err) } - for _, b := range behaviors { - if b.Name == name { - return b, nil - } - } - return nil, fmt.Errorf("could not find any Behavior with name '%s' in Defined Entity Type with ID '%s': %s", name, rdeType.DefinedEntityType.ID, ErrorEntityNotFound) + label := fmt.Sprintf("Defined Entity Behavior with name '%s' in Defined Entity Type with ID '%s': %s", name, rdeType.DefinedEntityType.ID, ErrorEntityNotFound) + return localFilterOneOrError(label, behaviors, "Name", name) } // UpdateBehaviorOverride overrides an Interface Behavior. Only Behavior description and execution can be overridden. @@ -261,23 +212,12 @@ func (rdeType *DefinedEntityType) UpdateBehaviorOverride(behavior types.Behavior return nil, fmt.Errorf("ID of the Behavior to override is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors - apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), behavior.ID) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, behavior.ID}, + entityLabel: labelRdeBehaviorOverride, } - response := types.Behavior{} - err = rdeType.client.OpenApiPutItem(apiVersion, urlRef, nil, behavior, &response, nil) - if err != nil { - return nil, err - } - - return &response, nil + return updateInnerEntity(rdeType.client, c, &behavior) } // DeleteBehaviorOverride removes a Behavior specified by its ID from the receiver Defined Entity Type. @@ -287,43 +227,28 @@ func (rdeType *DefinedEntityType) DeleteBehaviorOverride(behaviorId string) erro return fmt.Errorf("ID of the receiver Defined Entity Type is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors - apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), behaviorId) - if err != nil { - return err - } - err = rdeType.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, behaviorId}, + entityLabel: labelRdeBehaviorOverride, } - - return nil + return deleteEntityById(rdeType.client, c) } // SetBehaviorAccessControls sets the given slice of BehaviorAccess to the receiver Defined Entity Type. +// If the input is nil, it removes all access controls from the receiver Defined Entity Type. func (det *DefinedEntityType) SetBehaviorAccessControls(acls []*types.BehaviorAccess) error { if det.DefinedEntityType.ID == "" { return fmt.Errorf("ID of the receiver Defined Entity Type is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls - apiVersion, err := det.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := det.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, det.DefinedEntityType.ID)) - if err != nil { - return err + sanitizedAcls := acls + if acls == nil { + sanitizedAcls = []*types.BehaviorAccess{} } // Wrap it in OpenAPI pages, this endpoint requires it - rawMessage, err := json.Marshal(acls) + rawMessage, err := json.Marshal(sanitizedAcls) if err != nil { return fmt.Errorf("error setting Access controls in payload: %s", err) } @@ -331,7 +256,12 @@ func (det *DefinedEntityType) SetBehaviorAccessControls(acls []*types.BehaviorAc Values: rawMessage, } - err = det.client.OpenApiPutItem(apiVersion, urlRef, nil, payload, nil, nil) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls, + endpointParams: []string{det.DefinedEntityType.ID}, + entityLabel: labelRdeBehaviorAccessControl, + } + _, err = updateInnerEntity(det.client, c, &payload) if err != nil { return err } @@ -342,24 +272,13 @@ func (det *DefinedEntityType) SetBehaviorAccessControls(acls []*types.BehaviorAc // GetAllBehaviorsAccessControls gets all the Behaviors Access Controls from the receiver DefinedEntityType. // Query parameters can be supplied to modify pagination. func (det *DefinedEntityType) GetAllBehaviorsAccessControls(queryParameters url.Values) ([]*types.BehaviorAccess, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls - apiVersion, err := det.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := det.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, det.DefinedEntityType.ID)) - if err != nil { - return nil, err - } - - typeResponses := []*types.BehaviorAccess{{}} - err = det.client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls, + queryParameters: queryParameters, + endpointParams: []string{det.DefinedEntityType.ID}, + entityLabel: labelRdeBehaviorAccessControl, } - - return typeResponses, nil + return getAllInnerEntities[types.BehaviorAccess](det.client, c) } // GetAllRdes gets all the RDE instances of the given vendor, nss and version. @@ -375,33 +294,15 @@ func (rdeType *DefinedEntityType) GetAllRdes(queryParameters url.Values) ([]*Def // getAllRdes gets all the RDE instances of the given vendor, nss and version. // Supports filtering with the given queryParameters. func getAllRdes(client *Client, vendor, nss, version string, queryParameters url.Values) ([]*DefinedEntity, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/%s/%s", vendor, nss, version)) - if err != nil { - return nil, err - } - - typeResponses := []*types.DefinedEntity{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, err - } - - // Wrap all typeResponses into DefinedEntityType types with client - returnRDEs := make([]*DefinedEntity, len(typeResponses)) - for sliceIndex := range typeResponses { - returnRDEs[sliceIndex] = &DefinedEntity{ - DefinedEntity: typeResponses[sliceIndex], - client: client, - } + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes, + entityLabel: labelDefinedEntityType, + queryParameters: queryParameters, + endpointParams: []string{vendor, "/", nss, "/", version}, } - return returnRDEs, nil + outerType := DefinedEntity{client: client} + return getAllOuterEntities[DefinedEntity, types.DefinedEntity](client, outerType, c) } // GetRdesByName gets RDE instances with the given name that belongs to the receiver type. @@ -448,28 +349,18 @@ func (vcdClient *VCDClient) GetRdeById(id string) (*DefinedEntity, error) { // getRdeById gets a Runtime Defined Entity by its ID. // Getting a RDE by ID populates the ETag field in the returned object. func getRdeById(client *Client, id string) (*DefinedEntity, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + entityLabel: labelDefinedEntityType, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{id}, } - urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + outerType := DefinedEntity{client: client} + result, headers, err := getOuterEntityWithHeaders(client, outerType, c) if err != nil { return nil, err } - - result := &DefinedEntity{ - DefinedEntity: &types.DefinedEntity{}, - client: client, - } - - headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, nil, result.DefinedEntity, nil) - if err != nil { - return nil, amendRdeApiError(client, err) - } result.Etag = headers.Get("Etag") - return result, nil } @@ -480,23 +371,30 @@ func getRdeById(client *Client, id string) (*DefinedEntity, error) { // and the generated VCD task will remain at 1% until resolved. func (rdeType *DefinedEntityType) CreateRde(entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { entity.EntityType = rdeType.DefinedEntityType.ID - err := createRde(rdeType.client, entity, tenantContext) + task, err := createRde(rdeType.client, entity, tenantContext) if err != nil { return nil, err } - return pollPreCreatedRde(rdeType.client, rdeType.DefinedEntityType.Vendor, rdeType.DefinedEntityType.Nss, rdeType.DefinedEntityType.Version, entity.Name, 5) + return getRdeFromTask(rdeType.client, task) } // CreateRde creates an entity of the type of the given vendor, nss and version. // NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" // and the generated VCD task will remain at 1% until resolved. func (vcdClient *VCDClient) CreateRde(vendor, nss, version string, entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { + return createRdeAndGetFromTask(&vcdClient.Client, vendor, nss, version, entity, tenantContext) +} + +// createRdeAndGetFromTask creates an entity of the type of the given vendor, nss and version. +// NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" +// and the generated VCD task will remain at 1% until resolved. +func createRdeAndGetFromTask(client *Client, vendor, nss, version string, entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { entity.EntityType = fmt.Sprintf("urn:vcloud:type:%s:%s:%s", vendor, nss, version) - err := createRde(&vcdClient.Client, entity, tenantContext) + task, err := createRde(client, entity, tenantContext) if err != nil { return nil, err } - return pollPreCreatedRde(&vcdClient.Client, vendor, nss, version, entity.Name, 5) + return getRdeFromTask(client, task) } // CreateRde creates an entity of the type of the receiver Runtime Defined Entity (RDE) type. @@ -504,54 +402,58 @@ func (vcdClient *VCDClient) CreateRde(vendor, nss, version string, entity types. // it must match the type ID of the receiver RDE type. // NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" // and the generated VCD task will remain at 1% until resolved. -func createRde(client *Client, entity types.DefinedEntity, tenantContext *TenantContext) error { +func createRde(client *Client, entity types.DefinedEntity, tenantContext *TenantContext) (*Task, error) { if entity.EntityType == "" { - return fmt.Errorf("ID of the Runtime Defined Entity type is empty") + return nil, fmt.Errorf("ID of the Runtime Defined Entity type is empty") } if entity.Entity == nil || len(entity.Entity) == 0 { - return fmt.Errorf("the entity JSON is empty") + return nil, fmt.Errorf("the entity JSON is empty") } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { - return err + return nil, err } urlRef, err := client.OpenApiBuildEndpoint(endpoint, entity.EntityType) if err != nil { - return err + return nil, err } - _, err = client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, nil, entity, getTenantContextHeader(tenantContext)) + task, err := client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, nil, entity, getTenantContextHeader(tenantContext)) if err != nil { - return err + return nil, err } - return nil + // The refresh is needed as the task only has the HREF at the moment + err = task.Refresh() + if err != nil { + return nil, err + } + return &task, nil } -// pollPreCreatedRde polls VCD for a given amount of tries, to search for the RDE in state PRE_CREATED -// that corresponds to the given vendor, nss, version and name. -// This function can be useful on RDE creation, as VCD just returns a task that remains at 1% until the RDE is resolved, -// hence one needs to re-fetch the recently created RDE manually. -func pollPreCreatedRde(client *Client, vendor, nss, version, name string, tries int) (*DefinedEntity, error) { - var rdes []*DefinedEntity - var err error - for i := 0; i < tries; i++ { - rdes, err = getRdesByName(client, vendor, nss, version, name) - if err == nil { - for _, rde := range rdes { - // This doesn't really guarantee that the chosen RDE is the one we want, but there's no other way of - // fine-graining - if rde.DefinedEntity.State != nil && *rde.DefinedEntity.State == "PRE_CREATED" { - return rde, nil - } - } +// getRdeFromTask gets the Runtime Defined Entity from a given Task. This method is useful after RDE creation, as +// the API just returns a Task with the RDE details inside. +func getRdeFromTask(client *Client, task *Task) (*DefinedEntity, error) { + if task.Task == nil { + return nil, fmt.Errorf("could not retrieve the RDE from task, as it is nil") + } + rdeId := "" + if task.Task.Owner == nil { + // Try to retrieve the ID from the "Operation" field + beginning := strings.LastIndex(task.Task.Operation, "(") + end := strings.LastIndex(task.Task.Operation, ")") + if beginning < 0 || end < 0 || beginning >= end { + return nil, fmt.Errorf("could not retrieve the RDE from the task with ID '%s'", task.Task.ID) } - time.Sleep(3 * time.Second) + rdeId = task.Task.Operation[beginning+1 : end] + } else { + rdeId = task.Task.Owner.ID } - return nil, fmt.Errorf("could not create RDE, failed during retrieval after creation: %s", err) + + return getRdeById(client, rdeId) } // Resolve needs to be called after an RDE is successfully created. It makes the receiver RDE usable if the JSON entity @@ -584,8 +486,6 @@ func (rde *DefinedEntity) Resolve() error { // if rde.Resolve() failed and a JSON entity change is needed. // Updating a RDE populates the ETag field in the receiver object. func (rde *DefinedEntity) Update(rdeToUpdate types.DefinedEntity) error { - client := rde.client - if rde.DefinedEntity.ID == "" { return fmt.Errorf("ID of the receiver Runtime Defined Entity is empty") } @@ -608,21 +508,20 @@ func (rde *DefinedEntity) Update(rdeToUpdate types.DefinedEntity) error { rde.Etag = retrievedRde.Etag } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, rde.DefinedEntity.ID) - if err != nil { - return amendRdeApiError(client, err) + c := crudConfig{ + entityLabel: labelDefinedEntity, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{rde.DefinedEntity.ID}, + additionalHeader: map[string]string{"If-Match": rde.Etag}, } - headers, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, nil, rdeToUpdate, rde.DefinedEntity, map[string]string{"If-Match": rde.Etag}) + resultDefinedEntity, headers, err := updateInnerEntityWithHeaders(rde.client, c, &rdeToUpdate) if err != nil { return err } + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + rde.DefinedEntity = resultDefinedEntity rde.Etag = headers.Get("Etag") return nil @@ -630,26 +529,14 @@ func (rde *DefinedEntity) Update(rdeToUpdate types.DefinedEntity) error { // Delete deletes the receiver Runtime Defined Entity. func (rde *DefinedEntity) Delete() error { - client := rde.client - - if rde.DefinedEntity.ID == "" { - return fmt.Errorf("ID of the receiver Runtime Defined Entity is empty") + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{rde.DefinedEntity.ID}, + entityLabel: labelDefinedEntity, } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, rde.DefinedEntity.ID) - if err != nil { - return err - } - - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return amendRdeApiError(client, err) + if err := deleteEntityById(rde.client, c); err != nil { + return amendRdeApiError(rde.client, err) } rde.DefinedEntity = &types.DefinedEntity{} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_interface.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_interface.go index 873aa6b7e..f39ef4488 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_interface.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/defined_interface.go @@ -6,9 +6,15 @@ package govcd import ( "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const ( + labelDefinedInterface = "Defined Interface" + labelDefinedInterfaceBehavior = "Defined Interface Behavior" ) // DefinedInterface is a type for handling Defined Interfaces, from the Runtime Defined Entities framework, in VCD. @@ -18,64 +24,35 @@ type DefinedInterface struct { client *Client } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (d DefinedInterface) wrap(inner *types.DefinedInterface) *DefinedInterface { + d.DefinedInterface = inner + return &d +} + // CreateDefinedInterface creates a Defined Interface. // Only System administrator can create Defined Interfaces. func (vcdClient *VCDClient) CreateDefinedInterface(definedInterface *types.DefinedInterface) (*DefinedInterface, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - result := &DefinedInterface{ - DefinedInterface: &types.DefinedInterface{}, - client: &vcdClient.Client, + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + entityLabel: labelDefinedInterface, } - - err = client.OpenApiPostItem(apiVersion, urlRef, nil, definedInterface, result.DefinedInterface, nil) - if err != nil { - return nil, err - } - - return result, nil + outerType := DefinedInterface{client: &vcdClient.Client} + return createOuterEntity(&vcdClient.Client, outerType, c, definedInterface) } // GetAllDefinedInterfaces retrieves all Defined Interfaces. Query parameters can be supplied to perform additional filtering. func (vcdClient *VCDClient) GetAllDefinedInterfaces(queryParameters url.Values) ([]*DefinedInterface, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - typeResponses := []*types.DefinedInterface{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, amendRdeApiError(&client, err) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + entityLabel: labelDefinedInterface, + queryParameters: queryParameters, } - // Wrap all typeResponses into DefinedEntityType types with client - returnRDEs := make([]*DefinedInterface, len(typeResponses)) - for sliceIndex := range typeResponses { - returnRDEs[sliceIndex] = &DefinedInterface{ - DefinedInterface: typeResponses[sliceIndex], - client: &vcdClient.Client, - } - } - - return returnRDEs, nil + outerType := DefinedInterface{client: &vcdClient.Client} + return getAllOuterEntities(&vcdClient.Client, outerType, c) } // GetDefinedInterface retrieves a single Defined Interface defined by its unique combination of vendor, nss and version. @@ -100,37 +77,19 @@ func (vcdClient *VCDClient) GetDefinedInterface(vendor, nss, version string) (*D // GetDefinedInterfaceById gets a Defined Interface identified by its unique URN. func (vcdClient *VCDClient) GetDefinedInterfaceById(id string) (*DefinedInterface, error) { - client := vcdClient.Client - - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) - if err != nil { - return nil, err - } - - result := &DefinedInterface{ - DefinedInterface: &types.DefinedInterface{}, - client: &vcdClient.Client, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, result.DefinedInterface, nil) - if err != nil { - return nil, amendRdeApiError(&client, err) + c := crudConfig{ + entityLabel: labelDefinedInterface, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{id}, } - return result, nil + outerType := DefinedInterface{client: &vcdClient.Client} + return getOuterEntity(&vcdClient.Client, outerType, c) } // Update updates the receiver Defined Interface with the values given by the input. // Only System administrator can update Defined Interfaces. func (di *DefinedInterface) Update(definedInterface types.DefinedInterface) error { - client := di.client - if di.DefinedInterface.ID == "" { return fmt.Errorf("ID of the receiver Defined Interface is empty") } @@ -144,50 +103,39 @@ func (di *DefinedInterface) Update(definedInterface types.DefinedInterface) erro definedInterface.Nss = di.DefinedInterface.Nss definedInterface.Vendor = di.DefinedInterface.Vendor - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterface, } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, di.DefinedInterface.ID) + resultDefinedInterface, err := updateInnerEntity(di.client, c, &definedInterface) if err != nil { return err } - - err = client.OpenApiPutItem(apiVersion, urlRef, nil, definedInterface, di.DefinedInterface, nil) - if err != nil { - return amendRdeApiError(client, err) - } - - return nil + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + di.DefinedInterface = resultDefinedInterface + return err } // Delete deletes the receiver Defined Interface. // Only System administrator can delete Defined Interfaces. func (di *DefinedInterface) Delete() error { - client := di.client - if di.DefinedInterface.ID == "" { return fmt.Errorf("ID of the receiver Defined Interface is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterface, } - urlRef, err := client.OpenApiBuildEndpoint(endpoint, di.DefinedInterface.ID) + err := deleteEntityById(di.client, c) if err != nil { return err } - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return amendRdeApiError(client, err) - } - di.DefinedInterface = &types.DefinedInterface{} return nil } @@ -199,24 +147,12 @@ func (di *DefinedInterface) AddBehavior(behavior types.Behavior) (*types.Behavio return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors - apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID)) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterfaceBehavior, } - - result := &types.Behavior{} - err = di.client.OpenApiPostItem(apiVersion, urlRef, nil, behavior, result, nil) - if err != nil { - return nil, err - } - - return result, nil + return createInnerEntity(di.client, c, &behavior) } // GetAllBehaviors retrieves all the Behaviors of the receiver Defined Interface. @@ -229,47 +165,24 @@ func (di *DefinedInterface) GetAllBehaviors(queryParameters url.Values) ([]*type // getAllBehaviors gets all the Behaviors from the object referenced by the input Object ID with the given OpenAPI endpoint. func getAllBehaviors(client *Client, objectId, openApiEndpoint string, queryParameters url.Values) ([]*types.Behavior, error) { - endpoint := types.OpenApiPathVersion1_0_0 + openApiEndpoint - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + openApiEndpoint, + entityLabel: labelDefinedInterfaceBehavior, + endpointParams: []string{objectId}, + queryParameters: queryParameters, } - - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, objectId)) - if err != nil { - return nil, err - } - - typeResponses := []*types.Behavior{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, err - } - - return typeResponses, nil + return getAllInnerEntities[types.Behavior](client, c) } // GetBehaviorById retrieves a unique Behavior that belongs to the receiver Defined Interface and is determined by the // input ID. func (di *DefinedInterface) GetBehaviorById(id string) (*types.Behavior, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors - apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, id}, + entityLabel: labelDefinedInterfaceBehavior, } - - urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), id) - if err != nil { - return nil, err - } - - response := types.Behavior{} - err = di.client.OpenApiGetItem(apiVersion, urlRef, nil, &response, nil) - if err != nil { - return nil, err - } - - return &response, nil + return getInnerEntity[types.Behavior](di.client, c) } // GetBehaviorByName retrieves a unique Behavior that belongs to the receiver Defined Interface and is named after @@ -279,12 +192,8 @@ func (di *DefinedInterface) GetBehaviorByName(name string) (*types.Behavior, err if err != nil { return nil, fmt.Errorf("could not get the Behaviors of the Defined Interface with ID '%s': %s", di.DefinedInterface.ID, err) } - for _, b := range behaviors { - if b.Name == name { - return b, nil - } - } - return nil, fmt.Errorf("could not find any Behavior with name '%s' in Defined Interface with ID '%s': %s", name, di.DefinedInterface.ID, ErrorEntityNotFound) + label := fmt.Sprintf("Defined Interface Behavior with name '%s' in Defined Interface with ID '%s': %s", name, di.DefinedInterface.ID, ErrorEntityNotFound) + return localFilterOneOrError(label, behaviors, "Name", name) } // UpdateBehavior updates a Behavior specified by the input. @@ -296,23 +205,12 @@ func (di *DefinedInterface) UpdateBehavior(behavior types.Behavior) (*types.Beha return nil, fmt.Errorf("ID of the Behavior to update is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors - apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), behavior.ID) - if err != nil { - return nil, err - } - response := types.Behavior{} - err = di.client.OpenApiPutItem(apiVersion, urlRef, nil, behavior, &response, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, behavior.ID}, + entityLabel: labelDefinedInterfaceBehavior, } - - return &response, nil + return updateInnerEntity(di.client, c, &behavior) } // DeleteBehavior removes a Behavior specified by its ID from the receiver Defined Interface. @@ -321,22 +219,12 @@ func (di *DefinedInterface) DeleteBehavior(behaviorId string) error { return fmt.Errorf("ID of the receiver Defined Interface is empty") } - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors - apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), behaviorId) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, behaviorId}, + entityLabel: labelDefinedInterfaceBehavior, } - err = di.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return err - } - - return nil + return deleteEntityById(di.client, c) } // amendRdeApiError fixes a wrong type of error returned by VCD API <= v36.0 on GET operations diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/disk.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/disk.go index 30732d72f..3f88702a3 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/disk.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/disk.go @@ -94,6 +94,9 @@ func (vdc *Vdc) CreateDisk(diskCreateParams *types.DiskCreateParams) (Task, erro return Task{}, errors.New("error cannot find disk creation task in API response") } task := NewTask(vdc.client) + if disk.Disk.Tasks == nil || len(disk.Disk.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after disk %s creation", diskCreateParams.Disk.Name) + } task.Task = disk.Disk.Tasks.Task[0] util.Logger.Printf("[TRACE] AFTER CREATE DISK\n %s\n", prettyDisk(*disk.Disk)) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/generic_functions.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/generic_functions.go index 80b9e39ac..da681dcf7 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/generic_functions.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/generic_functions.go @@ -2,6 +2,7 @@ package govcd import ( "fmt" + "reflect" ) // oneOrError is used to cover up a common pattern in this codebase which is usually used in @@ -26,15 +27,72 @@ import ( // return nil, fmt.Errorf("more than one (%d) NSX-T Edge Cluster with name '%s' for Org VDC with id '%s' found", // len(nsxtEdgeClusters), name, vdc.Vdc.ID) // } -func oneOrError[T any](key, name string, entitySlice []*T) (*T, error) { +func oneOrError[E any](key, value string, entitySlice []*E) (*E, error) { if len(entitySlice) > 1 { - return nil, fmt.Errorf("got more than one entity by %s '%s' %d", key, name, len(entitySlice)) + return nil, fmt.Errorf("got more than one entity by %s '%s' %d", key, value, len(entitySlice)) } if len(entitySlice) == 0 { // No entity found - returning ErrorEntityNotFound as it must be wrapped in the returned error - return nil, fmt.Errorf("%s: got zero entities by %s '%s'", ErrorEntityNotFound, key, name) + return nil, fmt.Errorf("%s: got zero entities by %s '%s'", ErrorEntityNotFound, key, value) } return entitySlice[0], nil } + +// localFilter performs filtering of a type E based on a field name `fieldName` and its +// expected string value `expectedFieldValue`. Common use case for GetAllX methods where API does +// not support filtering and it must be done on the client side. +// +// Note. The field name `fieldName` must be present in a given type E (letter casing is important) +func localFilter[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) ([]*E, error) { + if len(entities) == 0 { + return nil, fmt.Errorf("zero entities provided for filtering") + } + + filteredValues := make([]*E, 0) + for _, entity := range entities { + + // Need to deference pointer because `reflect` package requires to work with types and not + // pointers to types + var entityValue E + if entity != nil { + entityValue = *entity + } else { + return nil, fmt.Errorf("given entity for %s is a nil pointer", entityLabel) + } + + value := reflect.ValueOf(entityValue) + field := value.FieldByName(fieldName) + + if !field.IsValid() { + return nil, fmt.Errorf("the struct for %s does not have the field '%s'", entityLabel, fieldName) + } + + if field.Type().Name() != "string" { + return nil, fmt.Errorf("field '%s' is not string type, it has type '%s'", fieldName, field.Type().Name()) + } + + if field.String() == expectedFieldValue { + filteredValues = append(filteredValues, entity) + } + } + + return filteredValues, nil +} + +// localFilterOneOrError performs local filtering using `genericLocalFilter()` and +// additionally verifies that only a single result is present using `oneOrError()`. Common use case +// for GetXByName methods where API does not support filtering and it must be done on client side. +func localFilterOneOrError[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) (*E, error) { + if fieldName == "" || expectedFieldValue == "" { + return nil, fmt.Errorf("expected field name and value must be specified to filter %s", entityLabel) + } + + filteredValues, err := localFilter(entityLabel, entities, fieldName, expectedFieldValue) + if err != nil { + return nil, err + } + + return oneOrError(fieldName, expectedFieldValue, filteredValues) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/importable_dvpg.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/importable_dvpg.go index 9dd04cc18..8218d3f6d 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/importable_dvpg.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/importable_dvpg.go @@ -120,3 +120,29 @@ func filterVcImportableDvpgsByName(name string, allNVcImportableDvpgs []*Vcenter return filteredVcImportableDvpgs } + +// Parent returns the port group parent switch +func (dvpg *VcenterImportableDvpg) Parent() *types.OpenApiReference { + return dvpg.VcenterImportableDvpg.DvSwitch.BackingRef +} + +// UsableWith tells whether a given port group can be used with others to create a network pool +func (dvpg *VcenterImportableDvpg) UsableWith(others ...*VcenterImportableDvpg) bool { + // No items provided: assume false + if len(others) == 0 { + return false + } + // Only one item provided, and it is the same as the current port group: assume false + if len(others) == 1 && dvpg.VcenterImportableDvpg.BackingRef.ID == others[0].VcenterImportableDvpg.BackingRef.ID { + return false + } + for _, other := range others { + if dvpg.VcenterImportableDvpg.BackingRef.ID == others[0].VcenterImportableDvpg.BackingRef.ID { + continue + } + if dvpg.Parent().ID != other.Parent().ID { + return false + } + } + return true +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space.go index ae740ea25..79c7944c3 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space.go @@ -1,5 +1,5 @@ /* - * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -11,6 +11,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelIpSpace = "IP Space" + // IpSpace provides structured approach to allocating public and private IP addresses by preventing // the use of overlapping IP addresses across organizations and organization VDCs. // @@ -27,31 +29,22 @@ type IpSpace struct { vcdClient *VCDClient } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (g IpSpace) wrap(inner *types.IpSpace) *IpSpace { + g.IpSpace = inner + return &g +} + // CreateIpSpace creates IP Space with desired configuration func (vcdClient *VCDClient) CreateIpSpace(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + entityLabel: labelIpSpace, } - - result := &IpSpace{ - IpSpace: &types.IpSpace{}, - vcdClient: vcdClient, - } - - err = client.OpenApiPostItem(apiVersion, urlRef, nil, ipSpaceConfig, result.IpSpace, nil) - if err != nil { - return nil, err - } - - return result, nil + outerType := IpSpace{vcdClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, ipSpaceConfig) } // GetAllIpSpaceSummaries retrieve summaries of all IP Spaces with an optional filter @@ -59,65 +52,14 @@ func (vcdClient *VCDClient) CreateIpSpace(ipSpaceConfig *types.IpSpace) (*IpSpac // "summaries" endpoint exists, but it does not include all fields. To retrieve complete structure // one can use `GetIpSpaceById` or `GetIpSpaceByName` func (vcdClient *VCDClient) GetAllIpSpaceSummaries(queryParameters url.Values) ([]*IpSpace, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - typeResponses := []*types.IpSpace{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) - if err != nil { - return nil, err - } - - // Wrap all typeResponses into IpSpace types with client - results := make([]*IpSpace, len(typeResponses)) - for sliceIndex := range typeResponses { - results[sliceIndex] = &IpSpace{ - IpSpace: typeResponses[sliceIndex], - vcdClient: vcdClient, - } - } - - return results, nil -} - -// GetIpSpaceById retrieves IP Space with a given ID -func (vcdClient *VCDClient) GetIpSpaceById(id string) (*IpSpace, error) { - if id == "" { - return nil, fmt.Errorf("IP Space lookup requires ID") - } - - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) - if err != nil { - return nil, err - } - - response := &IpSpace{ - vcdClient: vcdClient, - IpSpace: &types.IpSpace{}, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, response.IpSpace, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries, + entityLabel: labelIpSpace, + queryParameters: queryParameters, } - return response, nil + outerType := IpSpace{vcdClient: vcdClient} + return getAllOuterEntities[IpSpace, types.IpSpace](&vcdClient.Client, outerType, c) } // GetIpSpaceByName retrieves IP Space with a given name @@ -127,15 +69,15 @@ func (vcdClient *VCDClient) GetIpSpaceByName(name string) (*IpSpace, error) { return nil, fmt.Errorf("IP Space lookup requires name") } - queryParameters := url.Values{} - queryParameters.Add("filter", "name=="+name) + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) - filteredIpSpaces, err := vcdClient.GetAllIpSpaceSummaries(queryParameters) + filteredEntities, err := vcdClient.GetAllIpSpaceSummaries(queryParams) if err != nil { - return nil, fmt.Errorf("error getting IP Spaces: %s", err) + return nil, err } - singleIpSpace, err := oneOrError("name", name, filteredIpSpaces) + singleIpSpace, err := oneOrError("name", name, filteredEntities) if err != nil { return nil, err } @@ -143,6 +85,17 @@ func (vcdClient *VCDClient) GetIpSpaceByName(name string) (*IpSpace, error) { return vcdClient.GetIpSpaceById(singleIpSpace.IpSpace.ID) } +func (vcdClient *VCDClient) GetIpSpaceById(id string) (*IpSpace, error) { + c := crudConfig{ + entityLabel: labelIpSpace, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{id}, + } + + outerType := IpSpace{vcdClient: vcdClient} + return getOuterEntity[IpSpace, types.IpSpace](&vcdClient.Client, outerType, c) +} + // GetIpSpaceByNameAndOrgId retrieves IP Space with a given name in a particular Org // Note. Only PRIVATE IP spaces belong to Orgs func (vcdClient *VCDClient) GetIpSpaceByNameAndOrgId(name, orgId string) (*IpSpace, error) { @@ -150,16 +103,16 @@ func (vcdClient *VCDClient) GetIpSpaceByNameAndOrgId(name, orgId string) (*IpSpa return nil, fmt.Errorf("IP Space lookup requires name and Org ID") } - queryParameters := url.Values{} - queryParameters.Add("filter", "name=="+name) - queryParameters = queryParameterFilterAnd("orgRef.id=="+orgId, queryParameters) + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + queryParams = queryParameterFilterAnd("orgRef.id=="+orgId, queryParams) - filteredIpSpaces, err := vcdClient.GetAllIpSpaceSummaries(queryParameters) + filteredEntities, err := vcdClient.GetAllIpSpaceSummaries(queryParams) if err != nil { - return nil, fmt.Errorf("error getting IP Spaces: %s", err) + return nil, err } - singleIpSpace, err := oneOrError("name", name, filteredIpSpaces) + singleIpSpace, err := oneOrError("name", name, filteredEntities) if err != nil { return nil, err } @@ -169,58 +122,21 @@ func (vcdClient *VCDClient) GetIpSpaceByNameAndOrgId(name, orgId string) (*IpSpa // Update updates IP Space with new config func (ipSpace *IpSpace) Update(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { - client := ipSpace.vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - ipSpaceConfig.ID = ipSpace.IpSpace.ID - urlRef, err := client.OpenApiBuildEndpoint(endpoint, ipSpaceConfig.ID) - if err != nil { - return nil, err - } - - returnIpSpace := &IpSpace{ - IpSpace: &types.IpSpace{}, - vcdClient: ipSpace.vcdClient, - } - - err = client.OpenApiPutItem(apiVersion, urlRef, nil, ipSpaceConfig, returnIpSpace.IpSpace, nil) - if err != nil { - return nil, fmt.Errorf("error updating IP Space: %s", err) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{ipSpace.IpSpace.ID}, + entityLabel: labelIpSpace, } - - return returnIpSpace, nil + outerType := IpSpace{vcdClient: ipSpace.vcdClient} + return updateOuterEntity(&ipSpace.vcdClient.Client, outerType, c, ipSpaceConfig) } // Delete deletes IP Space func (ipSpace *IpSpace) Delete() error { - if ipSpace == nil || ipSpace.IpSpace == nil || ipSpace.IpSpace.ID == "" { - return fmt.Errorf("IP Space must have ID") - } - - client := ipSpace.vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{ipSpace.IpSpace.ID}, + entityLabel: labelIpSpace, } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, ipSpace.IpSpace.ID) - if err != nil { - return err - } - - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return err - } - - if err != nil { - return fmt.Errorf("error deleting IP space: %s", err) - } - - return nil + return deleteEntityById(&ipSpace.vcdClient.Client, c) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_allocation.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_allocation.go index 9df5e71d0..1940eed10 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_allocation.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_allocation.go @@ -12,6 +12,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelIpSpaceFloatingIpSuggestion = "IP Space floating IP suggestions" + // IpSpaceIpAllocation handles IP Space IP allocation requests type IpSpaceIpAllocation struct { IpSpaceIpAllocation *types.IpSpaceIpAllocation @@ -279,3 +281,30 @@ func getAllIpSpaceAllocations(client *Client, ipSpaceId string, org *Org, queryP return results, nil } + +// GetAllIpSpaceFloatingIpSuggestions suggests IP addresses to use for networking services on Edge +// Gateway or Provider Gateway. 'gatewayId' is mandatory. Based on the specified Gateway, VCD will +// query all the applicable IP Spaces and suggest some IP addresses which can be utilized to +// configure the network services on the Gateway. Allocated IP Space's IP addresses, but not +// currently used for any network services are returned. Results can also be filtered by IPV4 or +// IPV6 IP address types. +// +// Filter examples:(filter=gatewayId==URN), (filter=gatewayId==URN;ipType==IPV6) +// Go code: +// queryParams := url.Values{} +// queryParams.Set("filter", "ipType==IPV4") +func (vcdClient *VCDClient) GetAllIpSpaceFloatingIpSuggestions(gatewayId string, queryParameters url.Values) ([]*types.IpSpaceFloatingIpSuggestion, error) { + if gatewayId == "" { + return nil, fmt.Errorf("edge gateway ID is mandatory") + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("gatewayId=="+gatewayId, queryParams) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceFloatingIpSuggestions, + entityLabel: labelIpSpaceFloatingIpSuggestion, + queryParameters: queryParams, + } + + return getAllInnerEntities[types.IpSpaceFloatingIpSuggestion](&vcdClient.Client, c) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_uplink.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_uplink.go index e511b5fa8..5c5190e49 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_uplink.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/ip_space_uplink.go @@ -11,6 +11,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelIpSpaceUplink = "IP Space Uplink" + // IpSpaceUplink provides the capability to assign one or more IP Spaces as Uplinks to External // Networks type IpSpaceUplink struct { @@ -18,31 +20,23 @@ type IpSpaceUplink struct { vcdClient *VCDClient } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (i IpSpaceUplink) wrap(inner *types.IpSpaceUplink) *IpSpaceUplink { + i.IpSpaceUplink = inner + return &i +} + // CreateIpSpaceUplink creates an IP Space Uplink with a given configuration func (vcdClient *VCDClient) CreateIpSpaceUplink(ipSpaceUplinkConfig *types.IpSpaceUplink) (*IpSpaceUplink, error) { - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - result := &IpSpaceUplink{ - IpSpaceUplink: &types.IpSpaceUplink{}, - vcdClient: vcdClient, + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + entityLabel: labelIpSpaceUplink, } - err = client.OpenApiPostItem(apiVersion, urlRef, nil, ipSpaceUplinkConfig, result.IpSpaceUplink, nil) - if err != nil { - return nil, err - } - - return result, nil + outerType := IpSpaceUplink{vcdClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, ipSpaceUplinkConfig) } // GetAllIpSpaceUplinks retrieves all IP Space Uplinks for a given External Network ID @@ -54,35 +48,14 @@ func (vcdClient *VCDClient) GetAllIpSpaceUplinks(externalNetworkId string, query } queryparams := queryParameterFilterAnd(fmt.Sprintf("externalNetworkRef.id==%s", externalNetworkId), queryParameters) - - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } - - typeResponses := []*types.IpSpaceUplink{{}} - err = client.OpenApiGetAllItems(apiVersion, urlRef, queryparams, &typeResponses, nil) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + entityLabel: labelIpSpaceUplink, + queryParameters: queryparams, } - // Wrap all typeResponses into IpSpaceUplink types with client - results := make([]*IpSpaceUplink, len(typeResponses)) - for sliceIndex := range typeResponses { - results[sliceIndex] = &IpSpaceUplink{ - IpSpaceUplink: typeResponses[sliceIndex], - vcdClient: vcdClient, - } - } - - return results, nil + outerType := IpSpaceUplink{vcdClient: vcdClient} + return getAllOuterEntities[IpSpaceUplink, types.IpSpaceUplink](&vcdClient.Client, outerType, c) } // GetIpSpaceUplinkByName retrieves a single IP Space Uplink by Name in a given External Network @@ -98,61 +71,26 @@ func (vcdClient *VCDClient) GetIpSpaceUplinkByName(externalNetworkId, name strin // GetIpSpaceUplinkById retrieves IP Space Uplink with a given ID func (vcdClient *VCDClient) GetIpSpaceUplinkById(id string) (*IpSpaceUplink, error) { - if id == "" { - return nil, fmt.Errorf("IP Space Uplink lookup requires ID") - } - - client := vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{id}, + entityLabel: labelIpSpaceUplink, } - urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) - if err != nil { - return nil, err - } - - response := &IpSpaceUplink{ - vcdClient: vcdClient, - IpSpaceUplink: &types.IpSpaceUplink{}, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, response.IpSpaceUplink, nil) - if err != nil { - return nil, err - } - - return response, nil + outerType := IpSpaceUplink{vcdClient: vcdClient} + return getOuterEntity[IpSpaceUplink, types.IpSpaceUplink](&vcdClient.Client, outerType, c) } // Update IP Space Uplink func (ipSpaceUplink *IpSpaceUplink) Update(ipSpaceUplinkConfig *types.IpSpaceUplink) (*IpSpaceUplink, error) { - client := ipSpaceUplink.vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - ipSpaceUplinkConfig.ID = ipSpaceUplink.IpSpaceUplink.ID - urlRef, err := client.OpenApiBuildEndpoint(endpoint, ipSpaceUplinkConfig.ID) - if err != nil { - return nil, err - } - - result := &IpSpaceUplink{ - IpSpaceUplink: &types.IpSpaceUplink{}, - vcdClient: ipSpaceUplink.vcdClient, + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{ipSpaceUplink.IpSpaceUplink.ID}, + entityLabel: labelIpSpaceUplink, } - err = client.OpenApiPutItem(apiVersion, urlRef, nil, ipSpaceUplinkConfig, result.IpSpaceUplink, nil) - if err != nil { - return nil, fmt.Errorf("error updating IP Space: %s", err) - } - - return result, nil + outerType := IpSpaceUplink{vcdClient: ipSpaceUplink.vcdClient} + return updateOuterEntity(&ipSpaceUplink.vcdClient.Client, outerType, c, ipSpaceUplinkConfig) } // Delete IP Space Uplink @@ -161,26 +99,11 @@ func (ipSpaceUplink *IpSpaceUplink) Delete() error { return fmt.Errorf("IP Space Uplink must have ID") } - client := ipSpaceUplink.vcdClient.Client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - urlRef, err := client.OpenApiBuildEndpoint(endpoint, ipSpaceUplink.IpSpaceUplink.ID) - if err != nil { - return err - } - - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return err - } - - if err != nil { - return fmt.Errorf("error deleting IP Space Uplink: %s", err) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{ipSpaceUplink.IpSpaceUplink.ID}, + entityLabel: labelIpSpaceUplink, } - return nil + return deleteEntityById(&ipSpaceUplink.vcdClient.Client, c) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go index 6712dcb5d..3be745437 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/media.go @@ -747,3 +747,99 @@ func (vdc *Vdc) QueryAllMedia(mediaName string) ([]*MediaRecord, error) { util.Logger.Printf("[TRACE] Found media records by name: %#v \n", mediaResults) return newMediaRecords, nil } + +// enableDownload prepares a media item for download and returns a download link +// Note: depending on the size of the item, it may take a long time. +func (media *Media) enableDownload() (string, error) { + downloadUrl := getUrlFromLink(media.Media.Link, "enable", "") + if downloadUrl == "" { + return "", fmt.Errorf("no enable URL found") + } + // The result of this operation is the creation of an entry in the 'Files' field of the media structure + // Inside that field, there will be a Link entry with the URL for the download + // e.g. + // + // + // + // + // + task, err := media.client.executeTaskRequest( + downloadUrl, + http.MethodPost, + types.MimeTask, + "error enabling download: %s", + nil, + media.client.APIVersion) + if err != nil { + return "", err + } + err = task.WaitTaskCompletion() + if err != nil { + return "", err + } + + err = media.Refresh() + if err != nil { + return "", err + } + + if media.Media.Files == nil || len(media.Media.Files.File) == 0 { + return "", fmt.Errorf("no downloadable file info found") + } + downloadHref := "" + for _, f := range media.Media.Files.File { + for _, l := range f.Link { + if l.Rel == "download:default" { + downloadHref = l.HREF + break + } + if downloadHref != "" { + break + } + } + } + + if downloadHref == "" { + return "", fmt.Errorf("no download URL found") + } + + return downloadHref, nil +} + +// Download gets the contents of a media item as a byte stream +// NOTE: the whole item will be saved in local memory. Do not attempt this operation for very large items +func (media *Media) Download() ([]byte, error) { + + downloadHref, err := media.enableDownload() + if err != nil { + return nil, err + } + + downloadUrl, err := url.ParseRequestURI(downloadHref) + if err != nil { + return nil, fmt.Errorf("error getting download URL: %s", err) + } + + request := media.client.NewRequest(map[string]string{}, http.MethodGet, *downloadUrl, nil) + resp, err := media.client.Http.Do(request) + if err != nil { + return nil, fmt.Errorf("error getting media download: %s", err) + } + + if !isSuccessStatus(resp.StatusCode) { + return nil, fmt.Errorf("error downloading media: %s", resp.Status) + } + body, err := io.ReadAll(resp.Body) + + defer func() { + err = resp.Body.Close() + if err != nil { + panic(fmt.Sprintf("error closing body: %s", err)) + } + }() + + if err != nil { + return nil, err + } + return body, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_openapi.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_openapi.go new file mode 100644 index 000000000..f60c79ea8 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_openapi.go @@ -0,0 +1,298 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" + "strings" +) + +// OpenApiMetadataEntry is a wrapper object for types.OpenApiMetadataEntry +type OpenApiMetadataEntry struct { + MetadataEntry *types.OpenApiMetadataEntry + client *Client + Etag string // Allows concurrent operations with metadata + href string // This is the HREF of the given metadata entry + parentEndpoint string // This is the endpoint of the object that has the metadata entries +} + +// --------------------------------------------------------------------------------------------------------------------- +// Specific objects compatible with metadata +// --------------------------------------------------------------------------------------------------------------------- + +// GetMetadata returns all the metadata from a DefinedEntity. +// NOTE: The obtained metadata doesn't have ETags, use GetMetadataById or GetMetadataByKey to obtain a ETag for a specific entry. +func (rde *DefinedEntity) GetMetadata() ([]*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getAllOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", nil) +} + +// GetMetadataByKey returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +// This is a more costly operation than GetMetadataById due to ETags, so use that preferred option whenever possible. +func (rde *DefinedEntity) GetMetadataByKey(domain, namespace, key string) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getOpenApiMetadataByKey(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", domain, namespace, key) +} + +// GetMetadataById returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +func (rde *DefinedEntity) GetMetadataById(id string) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getOpenApiMetadataById(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", id) +} + +// AddMetadata adds metadata to the receiver DefinedEntity. +func (rde *DefinedEntity) AddMetadata(metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return addOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, metadataEntry) +} + +// --------------------------------------------------------------------------------------------------------------------- +// Metadata Entry methods for OpenAPI metadata +// --------------------------------------------------------------------------------------------------------------------- + +// Update updates the metadata value from the receiver entry. +// Only the value and persistence of the entry can be updated. Re-create the entry in case you want to modify any of the other fields. +func (entry *OpenApiMetadataEntry) Update(value interface{}, persistent bool) error { + if entry.MetadataEntry.ID == "" { + return fmt.Errorf("ID of the receiver metadata entry is empty") + } + + payload := types.OpenApiMetadataEntry{ + ID: entry.MetadataEntry.ID, + IsPersistent: persistent, + IsReadOnly: entry.MetadataEntry.IsReadOnly, + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: entry.MetadataEntry.KeyValue.Domain, + Key: entry.MetadataEntry.KeyValue.Key, + Value: types.OpenApiMetadataTypedValue{ + Value: value, + Type: entry.MetadataEntry.KeyValue.Value.Type, + }, + Namespace: entry.MetadataEntry.KeyValue.Namespace, + }, + } + + apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint) + if err != nil { + return err + } + + urlRef, err := url.ParseRequestURI(entry.href) + if err != nil { + return err + } + + headers, err := entry.client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, nil, payload, entry.MetadataEntry, map[string]string{"If-Match": entry.Etag}) + if err != nil { + return err + } + entry.Etag = headers.Get("Etag") + return nil +} + +// Delete deletes the receiver metadata entry. +func (entry *OpenApiMetadataEntry) Delete() error { + if entry.MetadataEntry.ID == "" { + return fmt.Errorf("ID of the receiver metadata entry is empty") + } + + apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint) + if err != nil { + return err + } + + urlRef, err := url.ParseRequestURI(entry.href) + if err != nil { + return err + } + + err = entry.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + entry.Etag = "" + entry.parentEndpoint = "" + entry.href = "" + entry.MetadataEntry = &types.OpenApiMetadataEntry{} + return nil +} + +// --------------------------------------------------------------------------------------------------------------------- +// OpenAPI Metadata private functions +// --------------------------------------------------------------------------------------------------------------------- + +// getAllOpenApiMetadata is a generic function to retrieve all metadata from any VCD object using its ID and the given OpenAPI endpoint. +// It supports query parameters to input, for example, filtering options. +func getAllOpenApiMetadata(client *Client, endpoint, objectId, objectName, objectType string, queryParameters url.Values) ([]*OpenApiMetadataEntry, error) { + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId)) + if err != nil { + return nil, err + } + + allMetadata := []*types.OpenApiMetadataEntry{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &allMetadata, nil) + if err != nil { + return nil, err + } + + var filteredMetadata []*types.OpenApiMetadataEntry + for _, entry := range allMetadata { + _, err = filterSingleOpenApiMetadataEntry(objectType, objectName, entry, client.IgnoredMetadata) + if err != nil { + if strings.Contains(err.Error(), "is being ignored") { + continue + } + return nil, err + } + filteredMetadata = append(filteredMetadata, entry) + } + + // Wrap all type.OpenApiMetadataEntry into OpenApiMetadataEntry types with client + results := make([]*OpenApiMetadataEntry, len(filteredMetadata)) + for i := range filteredMetadata { + results[i] = &OpenApiMetadataEntry{ + MetadataEntry: filteredMetadata[i], + client: client, + href: fmt.Sprintf("%s/%s", urlRef.String(), filteredMetadata[i].ID), + parentEndpoint: endpoint, + } + } + + return results, nil +} + +// getOpenApiMetadataByKey is a generic function to retrieve a unique metadata entry from any VCD object using its domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +func getOpenApiMetadataByKey(client *Client, endpoint, objectId, objectName, objectType string, domain, namespace, key string) (*OpenApiMetadataEntry, error) { + queryParameters := url.Values{} + // As for now, the filter only supports filtering by key + queryParameters.Add("filter", fmt.Sprintf("keyValue.key==%s", key)) + metadata, err := getAllOpenApiMetadata(client, endpoint, objectId, objectName, objectType, queryParameters) + if err != nil { + return nil, err + } + + if len(metadata) == 0 { + return nil, fmt.Errorf("%s could not find the metadata associated to object %s", ErrorEntityNotFound, objectId) + } + + // There's more than one entry with same key, the namespace and domain need to be compared to be able to filter. + if len(metadata) > 1 { + var filteredMetadata []*OpenApiMetadataEntry + for _, entry := range metadata { + if entry.MetadataEntry.KeyValue.Namespace == namespace && entry.MetadataEntry.KeyValue.Domain == domain { + filteredMetadata = append(filteredMetadata, entry) + } + } + if len(filteredMetadata) > 1 { + return nil, fmt.Errorf("found %d metadata entries associated to object %s", len(filteredMetadata), objectId) + } + // Required to retrieve an ETag + return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, filteredMetadata[0].MetadataEntry.ID) + } + + // Required to retrieve an ETag + return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, metadata[0].MetadataEntry.ID) +} + +// getOpenApiMetadataById is a generic function to retrieve a unique metadata entry from any VCD object using its unique ID. +func getOpenApiMetadataById(client *Client, endpoint, objectId, objectName, objectType, metadataId string) (*OpenApiMetadataEntry, error) { + if metadataId == "" { + return nil, fmt.Errorf("input metadata entry ID is empty") + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata/%s", objectId, metadataId)) + if err != nil { + return nil, err + } + + response := &OpenApiMetadataEntry{ + MetadataEntry: &types.OpenApiMetadataEntry{}, + client: client, + href: urlRef.String(), + parentEndpoint: endpoint, + } + + headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, nil, response.MetadataEntry, nil) + if err != nil { + return nil, err + } + + _, err = filterSingleOpenApiMetadataEntry(objectType, objectName, response.MetadataEntry, client.IgnoredMetadata) + if err != nil { + return nil, err + } + + response.Etag = headers.Get("Etag") + return response, nil +} + +// addOpenApiMetadata adds one metadata entry to the VCD object with given ID +func addOpenApiMetadata(client *Client, endpoint, objectId string, metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) { + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId)) + if err != nil { + return nil, err + } + + response := &OpenApiMetadataEntry{ + client: client, + MetadataEntry: &types.OpenApiMetadataEntry{}, + parentEndpoint: endpoint, + } + headers, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, metadataEntry, response.MetadataEntry, nil) + if err != nil { + return nil, err + } + response.Etag = headers.Get("Etag") + response.href = fmt.Sprintf("%s/%s", urlRef.String(), response.MetadataEntry.ID) + return response, nil +} + +// --------------------------------------------------------------------------------------------------------------------- +// Ignore OpenAPI Metadata feature +// --------------------------------------------------------------------------------------------------------------------- + +// normaliseOpenApiMetadata transforms OpenAPI metadata into a normalised structure +func normaliseOpenApiMetadata(objectType, name string, metadataEntry *types.OpenApiMetadataEntry) (*normalisedMetadata, error) { + return &normalisedMetadata{ + ObjectType: objectType, + ObjectName: name, + Key: metadataEntry.KeyValue.Key, + Value: fmt.Sprintf("%v", metadataEntry.KeyValue.Value.Value), + }, nil +} + +// filterSingleOpenApiMetadataEntry filters a single OpenAPI metadata entry depending on the contents of the input ignored metadata slice. +func filterSingleOpenApiMetadataEntry(objectType, objectName string, metadataEntry *types.OpenApiMetadataEntry, metadataToIgnore []IgnoredMetadata) (*types.OpenApiMetadataEntry, error) { + normalisedEntry, err := normaliseOpenApiMetadata(objectType, objectName, metadataEntry) + if err != nil { + return nil, err + } + isFiltered := filterSingleGenericMetadataEntry(normalisedEntry, metadataToIgnore) + if isFiltered { + return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", metadataEntry.KeyValue.Key, metadataEntry.KeyValue.Value.Value) + } + return metadataEntry, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_v2.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_v2.go index 8958ef8fd..7cc766de0 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_v2.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/metadata_v2.go @@ -759,7 +759,7 @@ func getMetadataByKey(client *Client, requestUri, name, key string, isSystem boo if err != nil { return nil, err } - return filterSingleMetadataEntry(key, requestUri, name, metadata, client.IgnoredMetadata) + return filterSingleXmlMetadataEntry(key, requestUri, name, metadata, client.IgnoredMetadata) } // getMetadata is a generic function to retrieve metadata from VCD @@ -770,7 +770,7 @@ func getMetadata(client *Client, requestUri, name string) (*types.Metadata, erro if err != nil { return nil, err } - return filterMetadata(metadata, requestUri, name, client.IgnoredMetadata) + return filterXmlMetadata(metadata, requestUri, name, client.IgnoredMetadata) } // addMetadata adds metadata to an entity. @@ -804,7 +804,7 @@ func addMetadata(client *Client, requestUri, name, key, value, typedValue, visib } } - _, err := filterSingleMetadataEntry(key, requestUri, name, newMetadata, client.IgnoredMetadata) + _, err := filterSingleXmlMetadataEntry(key, requestUri, name, newMetadata, client.IgnoredMetadata) if err != nil { return Task{}, err } @@ -856,7 +856,7 @@ func mergeAllMetadata(client *Client, requestUri, name string, metadata map[stri apiEndpoint := urlParseRequestURI(requestUri) apiEndpoint.Path += "/metadata" - filteredMetadata, err := filterMetadata(newMetadata, requestUri, name, client.IgnoredMetadata) + filteredMetadata, err := filterXmlMetadata(newMetadata, requestUri, name, client.IgnoredMetadata) if err != nil { return Task{}, err } @@ -935,9 +935,33 @@ func (im IgnoredMetadata) String() string { return fmt.Sprintf("IgnoredMetadata(ObjectType=%v, ObjectName=%v, KeyRegex=%v, ValueRegex=%v)", objectType, objectName, im.KeyRegex, im.ValueRegex) } -// filterMetadata filters all metadata entries, given a slice of metadata that needs to be ignored. It doesn't +// normalisedMetadata is an auxiliary type that allows to transform XML and OpenAPI metadata into a common structure +// for operations that are executed the same way in both flavors. +type normalisedMetadata struct { + ObjectType string + ObjectName string + Key string + Value string +} + +// normaliseXmlMetadata transforms XML metadata into a normalised structure +func normaliseXmlMetadata(key, href, objectName string, metadataEntry *types.MetadataValue) (*normalisedMetadata, error) { + objectType, err := getMetadataObjectTypeFromHref(href) + if err != nil { + return nil, err + } + + return &normalisedMetadata{ + ObjectType: objectType, + ObjectName: objectName, + Key: key, + Value: metadataEntry.TypedValue.Value, + }, nil +} + +// filterXmlMetadata filters all metadata entries, given a slice of metadata that needs to be ignored. It doesn't // alter the input metadata, but returns a copy of the filtered metadata. -func filterMetadata(allMetadata *types.Metadata, href, objectName string, metadataToIgnore []IgnoredMetadata) (*types.Metadata, error) { +func filterXmlMetadata(allMetadata *types.Metadata, href, objectName string, metadataToIgnore []IgnoredMetadata) (*types.Metadata, error) { if len(metadataToIgnore) == 0 { return allMetadata, nil } @@ -954,43 +978,55 @@ func filterMetadata(allMetadata *types.Metadata, href, objectName string, metada var filteredMetadata []*types.MetadataEntry for _, originalEntry := range allMetadata.MetadataEntry { - _, err := filterSingleMetadataEntry(originalEntry.Key, href, objectName, &types.MetadataValue{Domain: originalEntry.Domain, TypedValue: originalEntry.TypedValue}, metadataToIgnore) - if err == nil || !strings.Contains(err.Error(), "ignored") { - filteredMetadata = append(filteredMetadata, originalEntry) + _, err := filterSingleXmlMetadataEntry(originalEntry.Key, href, objectName, &types.MetadataValue{Domain: originalEntry.Domain, TypedValue: originalEntry.TypedValue}, metadataToIgnore) + if err != nil { + if strings.Contains(err.Error(), "ignored") { + continue + } + return nil, err } + filteredMetadata = append(filteredMetadata, originalEntry) } result.MetadataEntry = filteredMetadata return result, nil } -// filterSingleMetadataEntry filters a single metadata entry given a slice of metadata that needs to be ignored. It doesn't -// alter the input metadata, but returns a copy of the filtered metadata. -func filterSingleMetadataEntry(key, href, objectName string, metadataEntry *types.MetadataValue, metadataToIgnore []IgnoredMetadata) (*types.MetadataValue, error) { - if len(metadataToIgnore) == 0 { - return metadataEntry, nil - } - - objectType, err := getMetadataObjectTypeFromHref(href) +func filterSingleXmlMetadataEntry(key, href, objectName string, metadataEntry *types.MetadataValue, metadataToIgnore []IgnoredMetadata) (*types.MetadataValue, error) { + normalisedEntry, err := normaliseXmlMetadata(key, href, objectName, metadataEntry) if err != nil { return nil, err } + isFiltered := filterSingleGenericMetadataEntry(normalisedEntry, metadataToIgnore) + if isFiltered { + return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", key, metadataEntry.TypedValue.Value) + } + return metadataEntry, nil +} + +// filterSingleGenericMetadataEntry filters a single metadata entry given a slice of metadata that needs to be ignored. It doesn't +// alter the input metadata, but returns a bool that indicates whether the entry should be ignored or not. +func filterSingleGenericMetadataEntry(normalisedMetadataEntry *normalisedMetadata, metadataToIgnore []IgnoredMetadata) bool { + if len(metadataToIgnore) == 0 { + return false + } + for _, entryToIgnore := range metadataToIgnore { if entryToIgnore.ObjectType == nil && entryToIgnore.ObjectName == nil && entryToIgnore.KeyRegex == nil && entryToIgnore.ValueRegex == nil { continue } - util.Logger.Printf("[DEBUG] Comparing metadata with key '%s' with ignored metadata filter '%s'", key, entryToIgnore) + util.Logger.Printf("[DEBUG] Comparing metadata with key '%s' with ignored metadata filter '%s'", normalisedMetadataEntry.Key, entryToIgnore) // We apply an optimistic approach here to simplify the conditions, so the metadata entry will always be ignored unless the filters // tell otherwise, that is, if they are nil (not all of them as per the condition above), if they're empty or if they don't match. // All the filtering options (type, name, keyRegex and valueRegex) must compute to true for the metadata to be ignored. - if (entryToIgnore.ObjectType == nil || strings.TrimSpace(*entryToIgnore.ObjectType) == "" || *entryToIgnore.ObjectType == objectType) && - (entryToIgnore.ObjectName == nil || strings.TrimSpace(*entryToIgnore.ObjectName) == "" || strings.TrimSpace(objectName) == "" || *entryToIgnore.ObjectName == objectName) && - (entryToIgnore.KeyRegex == nil || entryToIgnore.KeyRegex.MatchString(key)) && - (entryToIgnore.ValueRegex == nil || entryToIgnore.ValueRegex.MatchString(metadataEntry.TypedValue.Value)) { - util.Logger.Printf("[DEBUG] the metadata entry with key '%s' and value '%v' is being ignored", key, metadataEntry.TypedValue.Value) - return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", key, metadataEntry.TypedValue.Value) + if (entryToIgnore.ObjectType == nil || strings.TrimSpace(*entryToIgnore.ObjectType) == "" || *entryToIgnore.ObjectType == normalisedMetadataEntry.ObjectType) && + (entryToIgnore.ObjectName == nil || strings.TrimSpace(*entryToIgnore.ObjectName) == "" || strings.TrimSpace(normalisedMetadataEntry.ObjectName) == "" || *entryToIgnore.ObjectName == normalisedMetadataEntry.ObjectName) && + (entryToIgnore.KeyRegex == nil || entryToIgnore.KeyRegex.MatchString(normalisedMetadataEntry.Key)) && + (entryToIgnore.ValueRegex == nil || entryToIgnore.ValueRegex.MatchString(normalisedMetadataEntry.Value)) { + util.Logger.Printf("[DEBUG] the metadata entry with key '%s' and value '%v' is being ignored", normalisedMetadataEntry.ObjectType, normalisedMetadataEntry.Value) + return true } } - return metadataEntry, nil + return false } // filterMetadataToDelete filters a metadata entry that is going to be deleted, given a slice of metadata that needs to be ignored. diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/network_pool.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/network_pool.go index cb5922db3..212303c91 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/network_pool.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/network_pool.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" + "strings" ) type NetworkPool struct { @@ -15,8 +16,14 @@ type NetworkPool struct { vcdClient *VCDClient } +var backingUseErrorMessages = map[types.BackingUseConstraint]string{ + types.BackingUseExplicit: "no element named %s found", + types.BackingUseWhenOnlyOne: "no single element found for this backing", + types.BackingUseFirstAvailable: "no elements found for this backing", +} + // GetOpenApiUrl retrieves the full URL of a network pool -func (np NetworkPool) GetOpenApiUrl() (string, error) { +func (np *NetworkPool) GetOpenApiUrl() (string, error) { response, err := url.JoinPath(np.vcdClient.sessionHREF.String(), "admin", "extension", "networkPool", np.NetworkPool.Id) if err != nil { return "", err @@ -102,3 +109,340 @@ func (vcdClient *VCDClient) GetNetworkPoolByName(name string) (*NetworkPool, err return vcdClient.GetNetworkPoolById(filteredNetworkPools[0].Id) } + +// CreateNetworkPool creates a network pool using the given configuration +// It can create any type of network pool +func (vcdClient *VCDClient) CreateNetworkPool(config *types.NetworkPool) (*NetworkPool, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + result := &NetworkPool{ + NetworkPool: &types.NetworkPool{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, config, result.NetworkPool, nil) + if err != nil { + return nil, err + } + + return result, nil +} + +// Update will change all changeable network pool items +func (np *NetworkPool) Update() error { + if np == nil || np.NetworkPool == nil || np.NetworkPool.Id == "" { + return fmt.Errorf("network pool must have ID") + } + if np.vcdClient == nil || np.vcdClient.Client.APIVersion == "" { + return fmt.Errorf("network pool '%s': no client found", np.NetworkPool.Name) + } + + client := np.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, np.NetworkPool.Id) + if err != nil { + return err + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, np.NetworkPool, np.NetworkPool, nil) + if err != nil { + return err + } + + if err != nil { + return fmt.Errorf("error updating network pool '%s': %s", np.NetworkPool.Name, err) + } + + return nil +} + +// Delete removes a network pool +func (np *NetworkPool) Delete() error { + if np == nil || np.NetworkPool == nil || np.NetworkPool.Id == "" { + return fmt.Errorf("network pool must have ID") + } + if np.vcdClient == nil || np.vcdClient.Client.APIVersion == "" { + return fmt.Errorf("network pool '%s': no client found", np.NetworkPool.Name) + } + + client := np.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, np.NetworkPool.Id) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + if err != nil { + return fmt.Errorf("error deleting network pool '%s': %s", np.NetworkPool.Name, err) + } + + return nil +} + +func backingErrorMessage(constraint types.BackingUseConstraint, name string) string { + errorMessage := fmt.Sprintf("[constraint: %s] %s", constraint, backingUseErrorMessages[constraint]) + if strings.Contains(errorMessage, "%s") { + return fmt.Sprintf(errorMessage, name) + } + return errorMessage +} + +type getElementFunc[B any] func(*B) string +type validElementFunc[B any] func(*B) bool + +// chooseBackingElement will select a backing element from a list, using the given constraint +// * constraint is the type of choice we are looking for +// * wantedName is the name of the element we want. If we use a constraint other than types.BackingUseExplicit, it can be empty +// * elements is the list of backing elements we want to choose from +// * getEl is a function that, given an element, returns its name +// * validateEl is an optional function that tells whether a given element is valid or not. If missing, we assume all elements are valid +func chooseBackingElement[B any](constraint types.BackingUseConstraint, wantedName string, elements []*B, getEl getElementFunc[B], validateEl validElementFunc[B]) (*B, error) { + var searchedElement *B + if validateEl == nil { + validateEl = func(*B) bool { return true } + } + numberOfValidElements := 0 + // We need to pre-calculate the number of valid elements, to use it when constraint == BackingUseWhenOnlyOne + for _, element := range elements { + if validateEl(element) { + numberOfValidElements++ + } + } + // availableElements will contain the list of available elements, to be used in error messages + var availableElements []string + for _, element := range elements { + elementName := getEl(element) + if !validateEl(element) { + continue + } + availableElements = append(availableElements, elementName) + + switch constraint { + case types.BackingUseExplicit: + // When asking for a specific element explicitly, we return it only if the name matches the request) + if wantedName == elementName { + searchedElement = element + } + case types.BackingUseWhenOnlyOne: + // With BackingUseWhenOnlyOne, we return the element only if there is a single *valid* element in the list + if wantedName == "" && numberOfValidElements == 1 { + searchedElement = element + } + case types.BackingUseFirstAvailable: + // This is the most permissive constraint: we get the first available element + if wantedName == "" { + searchedElement = element + } + } + if searchedElement != nil { + break + } + } + // If no item was retrieved, we build an error message appropriate for the current constraint, and add + // the list of available elements to it + if searchedElement == nil { + return nil, fmt.Errorf(backingErrorMessage(constraint, wantedName)+" - available elements: %v", availableElements) + } + + // When we reach this point, we have found what was requested, and return the element + return searchedElement, nil +} + +// CreateNetworkPoolGeneve creates a network pool of GENEVE type +// The function retrieves the given NSX-T manager and corresponding transport zone names +// If the transport zone name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolGeneve(name, description, nsxtManagerName, transportZoneName string, constraint types.BackingUseConstraint) (*NetworkPool, error) { + managers, err := vcdClient.QueryNsxtManagerByName(nsxtManagerName) + if err != nil { + return nil, err + } + + if len(managers) == 0 { + return nil, fmt.Errorf("no manager '%s' found", nsxtManagerName) + } + if len(managers) > 1 { + return nil, fmt.Errorf("more than one manager '%s' found", nsxtManagerName) + } + manager := managers[0] + + managerId := "urn:vcloud:nsxtmanager:" + extractUuid(managers[0].HREF) + transportZones, err := vcdClient.GetAllNsxtTransportZones(managerId, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving transport zones for manager '%s': %s", manager.Name, err) + } + transportZone, err := chooseBackingElement[types.TransportZone]( + constraint, + transportZoneName, + transportZones, + func(tz *types.TransportZone) string { return tz.Name }, + func(tz *types.TransportZone) bool { return !tz.AlreadyImported }, + ) + + if err != nil { + return nil, err + } + if transportZone.AlreadyImported { + return nil, fmt.Errorf("transport zone '%s' is already imported", transportZone.Name) + } + + // Note: in this type of network pool, the managing owner is the NSX-T manager + managingOwner := types.OpenApiReference{ + Name: manager.Name, + ID: managerId, + } + var config = &types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolGeneveType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + TransportZoneRef: types.OpenApiReference{ + ID: transportZone.Id, + Name: transportZone.Name, + }, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(config) +} + +// CreateNetworkPoolPortGroup creates a network pool of PORTGROUP_BACKED type +// The function retrieves the given vCenter and corresponding port group names +// If the port group name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolPortGroup(name, description, vCenterName string, portgroupNames []string, constraint types.BackingUseConstraint) (*NetworkPool, error) { + vCenter, err := vcdClient.GetVCenterByName(vCenterName) + if err != nil { + return nil, fmt.Errorf("error retrieving vCenter '%s': %s", vCenterName, err) + } + var params = make(url.Values) + params.Set("filter", "virtualCenter.id=="+vCenter.VSphereVCenter.VcId) + portgroups, err := vcdClient.GetAllVcenterImportableDvpgs(params) + if err != nil { + return nil, fmt.Errorf("error retrieving portgroups for vCenter '%s': %s", vCenterName, err) + } + + var chosenPortgroups []*VcenterImportableDvpg + var chosenReferences []types.OpenApiReference + for _, portgroupName := range portgroupNames { + portGroup, err := chooseBackingElement[VcenterImportableDvpg]( + constraint, + portgroupName, + portgroups, + func(v *VcenterImportableDvpg) string { + return v.VcenterImportableDvpg.BackingRef.Name + }, + nil, + ) + + if err != nil { + return nil, err + } + chosenPortgroups = append(chosenPortgroups, portGroup) + chosenReferences = append(chosenReferences, types.OpenApiReference{ + Name: portGroup.VcenterImportableDvpg.BackingRef.Name, + ID: portGroup.VcenterImportableDvpg.BackingRef.ID, + }) + } + + if len(chosenPortgroups) == 0 { + return nil, fmt.Errorf("no suitable portgroups found for names %v", portgroupNames) + } + if len(chosenPortgroups) > 1 { + if !chosenPortgroups[0].UsableWith(chosenPortgroups...) { + return nil, fmt.Errorf("portgroups %v should all belong to the same host", portgroupNames) + } + } + + // Note: in this type of network pool, the managing owner is the vCenter + managingOwner := types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + } + config := types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolPortGroupType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + PortGroupRefs: chosenReferences, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(&config) +} + +// CreateNetworkPoolVlan creates a network pool of VLAN type +// The function retrieves the given vCenter and corresponding distributed switch names +// If the distributed switch name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolVlan(name, description, vCenterName, dsName string, ranges []types.VlanIdRange, constraint types.BackingUseConstraint) (*NetworkPool, error) { + vCenter, err := vcdClient.GetVCenterByName(vCenterName) + if err != nil { + return nil, fmt.Errorf("error retrieving vCenter '%s': %s", vCenterName, err) + } + + dswitches, err := vcdClient.GetAllVcenterDistributedSwitches(vCenter.VSphereVCenter.VcId, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving distributed switches for vCenter '%s': %s", vCenterName, err) + } + + dswitch, err := chooseBackingElement[types.VcenterDistributedSwitch]( + constraint, + dsName, + dswitches, + func(t *types.VcenterDistributedSwitch) string { return t.BackingRef.Name }, + nil, + ) + if err != nil { + return nil, err + } + + // Note: in this type of network pool, the managing owner is the vCenter + managingOwner := types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + } + config := types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolVlanType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + VlanIdRanges: types.VlanIdRanges{ + Values: ranges, + }, + VdsRefs: []types.OpenApiReference{ + { + Name: dswitch.BackingRef.Name, + ID: dswitch.BackingRef.ID, + }, + }, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(&config) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_alb_controllers.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_alb_controllers.go index 34d7f3c1a..e15b3f922 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_alb_controllers.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_alb_controllers.go @@ -132,15 +132,7 @@ func (vcdClient *VCDClient) GetAlbControllerByUrl(url string) (*NsxtAlbControlle } } - if len(filteredControllers) == 0 { - return nil, fmt.Errorf("%s could not find ALB Controller by Url '%s'", ErrorEntityNotFound, url) - } - - if len(filteredControllers) > 1 { - return nil, fmt.Errorf("found more than 1 ALB Controller by Url '%s'", url) - } - - return filteredControllers[0], nil + return oneOrError("url", url, filteredControllers) } // CreateNsxtAlbController creates controller with supplied albControllerConfig configuration diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_distributed_firewall.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_distributed_firewall.go index 1d985c9a3..d5e605174 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_distributed_firewall.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_distributed_firewall.go @@ -10,6 +10,11 @@ import ( "github.com/vmware/go-vcloud-director/v2/util" ) +const ( + labelDistributedFirewall = "NSX-T Distributed Firewall" + labelDistributedFirewallRule = "NSX-T Distributed Firewall Rule" +) + // DistributedFirewall contains a types.DistributedFirewallRules which handles Distributed Firewall // rules in a VDC Group type DistributedFirewall struct { @@ -18,6 +23,14 @@ type DistributedFirewall struct { VdcGroup *VdcGroup } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (d DistributedFirewall) wrap(inner *types.DistributedFirewallRules) *DistributedFirewall { + d.DistributedFirewallRuleContainer = inner + return &d +} + // DistributedFirewallRule is a representation of a single rule type DistributedFirewallRule struct { Rule *types.DistributedFirewallRule @@ -25,36 +38,27 @@ type DistributedFirewallRule struct { VdcGroup *VdcGroup } +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (d DistributedFirewallRule) wrap(inner *types.DistributedFirewallRule) *DistributedFirewallRule { + d.Rule = inner + return &d +} + // GetDistributedFirewall retrieves Distributed Firewall in a VDC Group which contains all rules // // Note. This function works only with `default` policy as this was the only supported when this // functions was created func (vdcGroup *VdcGroup) GetDistributedFirewall() (*DistributedFirewall, error) { - client := vdcGroup.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault)) - if err != nil { - return nil, err - } - - returnObject := &DistributedFirewall{ - DistributedFirewallRuleContainer: &types.DistributedFirewallRules{}, - client: client, - VdcGroup: vdcGroup, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.DistributedFirewallRuleContainer, nil) - if err != nil { - return nil, fmt.Errorf("error retrieving Distributed Firewall rules: %s", err) + c := crudConfig{ + entityLabel: labelDistributedFirewall, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, } - return returnObject, nil + outerType := DistributedFirewall{client: vdcGroup.client, VdcGroup: vdcGroup} + return getOuterEntity[DistributedFirewall, types.DistributedFirewallRules](vdcGroup.client, outerType, c) } // UpdateDistributedFirewall updates Distributed Firewall in a VDC Group @@ -62,31 +66,14 @@ func (vdcGroup *VdcGroup) GetDistributedFirewall() (*DistributedFirewall, error) // Note. This function works only with `default` policy as this was the only supported when this // functions was created func (vdcGroup *VdcGroup) UpdateDistributedFirewall(dfwRules *types.DistributedFirewallRules) (*DistributedFirewall, error) { - client := vdcGroup.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault)) - if err != nil { - return nil, err - } - - returnObject := &DistributedFirewall{ - DistributedFirewallRuleContainer: &types.DistributedFirewallRules{}, - client: client, - VdcGroup: vdcGroup, - } - - err = client.OpenApiPutItem(apiVersion, urlRef, nil, dfwRules, returnObject.DistributedFirewallRuleContainer, nil) - if err != nil { - return nil, fmt.Errorf("error updating Distributed Firewall rules: %s", err) + c := crudConfig{ + entityLabel: labelDistributedFirewall, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, } - return returnObject, nil + outerType := DistributedFirewall{client: vdcGroup.client, VdcGroup: vdcGroup} + return updateOuterEntity[DistributedFirewall, types.DistributedFirewallRules](vdcGroup.client, outerType, c, dfwRules) } // DeleteAllDistributedFirewallRules removes all Distributed Firewall rules @@ -112,35 +99,14 @@ func (firewall *DistributedFirewall) DeleteAllRules() error { // GetDistributedFirewallRuleById retrieves single Distributed Firewall Rule by ID func (vdcGroup *VdcGroup) GetDistributedFirewallRuleById(id string) (*DistributedFirewallRule, error) { - if id == "" { - return nil, fmt.Errorf("id must be specified") - } - - client := vdcGroup.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", id}, } - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", id) - if err != nil { - return nil, err - } - - returnObject := &DistributedFirewallRule{ - Rule: &types.DistributedFirewallRule{}, - client: client, - VdcGroup: vdcGroup, - } - - err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.Rule, nil) - if err != nil { - return nil, fmt.Errorf("error retrieving Distributed Firewall rule: %s", err) - } - - return returnObject, nil + outerType := DistributedFirewallRule{client: vdcGroup.client, VdcGroup: vdcGroup} + return getOuterEntity[DistributedFirewallRule, types.DistributedFirewallRule](vdcGroup.client, outerType, c) } // GetDistributedFirewallRuleByName retrieves single firewall rule by name @@ -154,19 +120,12 @@ func (vdcGroup *VdcGroup) GetDistributedFirewallRuleByName(name string) (*Distri return nil, fmt.Errorf("error returning distributed firewall rules: %s", err) } - var filteredByName []*types.DistributedFirewallRule - for _, rule := range dfw.DistributedFirewallRuleContainer.Values { - if rule.Name == name { - filteredByName = append(filteredByName, rule) - } - } - - oneByName, err := oneOrError("name", name, filteredByName) + singleRuleByName, err := localFilterOneOrError(labelDistributedFirewallRule, dfw.DistributedFirewallRuleContainer.Values, "Name", name) if err != nil { return nil, err } - return vdcGroup.GetDistributedFirewallRuleById(oneByName.ID) + return vdcGroup.GetDistributedFirewallRuleById(singleRuleByName.ID) } // CreateDistributedFirewallRule is a non-thread safe wrapper around @@ -193,27 +152,19 @@ func (vdcGroup *VdcGroup) GetDistributedFirewallRuleByName(name string) (*Distri // Note. Running this function concurrently will corrupt firewall rules as it uses an endpoint that // manages all rules ("vdcGroups/%s/dfwPolicies/%s/rules") func (vdcGroup *VdcGroup) CreateDistributedFirewallRule(optionalAboveRuleId string, rule *types.DistributedFirewallRule) (*DistributedFirewall, *DistributedFirewallRule, error) { - client := vdcGroup.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, nil, err - } - - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault)) - if err != nil { - return nil, nil, err - } - // 1. Getting all Distributed Firewall Rules and storing them in private intermediate // type`distributedFirewallRulesRaw` which holds a []json.RawMessage (text) instead of exact types. // This will prevent altering existing rules in any way (for example if a new field appears in // schema in future VCD versions) - rawJsonExistingFirewallRules := &distributedFirewallRulesRaw{} - err = client.OpenApiGetItem(apiVersion, urlRef, nil, rawJsonExistingFirewallRules, nil) + + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + rawJsonExistingFirewallRules, err := getInnerEntity[distributedFirewallRulesRaw](vdcGroup.client, c) if err != nil { - return nil, nil, fmt.Errorf("error retrieving Distributed Firewall rules in raw format: %s", err) + return nil, nil, err } // 2. Converting the give `rule` (*types.DistributedFirewallRule) into json.RawMessage so that @@ -265,22 +216,32 @@ func (vdcGroup *VdcGroup) CreateDistributedFirewallRule(optionalAboveRuleId stri Values: dfwRuleUpdatePayload, } - returnAllFirewallRules := &DistributedFirewall{ - DistributedFirewallRuleContainer: &types.DistributedFirewallRules{}, - client: client, - VdcGroup: vdcGroup, + c2 := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + + updatedFirewallRules, err := updateInnerEntity(vdcGroup.client, c2, updateRequestPayload) + if err != nil { + return nil, nil, err } - err = client.OpenApiPutItem(apiVersion, urlRef, nil, updateRequestPayload, returnAllFirewallRules.DistributedFirewallRuleContainer, nil) + dfwResults, err := convertRawJsonToFirewallRules(updatedFirewallRules) if err != nil { - return nil, nil, fmt.Errorf("error updating Distributed Firewall rules: %s", err) + return nil, nil, err } - // Create an entity for single firewall rule (which can be updated and deleted using their own endpoints) returnObjectSingleRule := &DistributedFirewallRule{ - Rule: returnAllFirewallRules.DistributedFirewallRuleContainer.Values[newRuleSlicePosition], - client: client, + client: vdcGroup.client, VdcGroup: vdcGroup, + Rule: dfwResults.Values[newRuleSlicePosition], + } + + returnAllFirewallRules := &DistributedFirewall{ + DistributedFirewallRuleContainer: dfwResults, + client: vdcGroup.client, + VdcGroup: vdcGroup, } return returnAllFirewallRules, returnObjectSingleRule, nil @@ -288,63 +249,23 @@ func (vdcGroup *VdcGroup) CreateDistributedFirewallRule(optionalAboveRuleId stri // Update a single Distributed Firewall Rule func (dfwRule *DistributedFirewallRule) Update(rule *types.DistributedFirewallRule) (*DistributedFirewallRule, error) { - if dfwRule.Rule.ID == "" { - return nil, fmt.Errorf("cannot update NSX-T Distribute Firewall Rule without ID") - } - - client := dfwRule.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", dfwRule.Rule.ID) - if err != nil { - return nil, err - } - - returnObjectSingleRule := &DistributedFirewallRule{ - Rule: &types.DistributedFirewallRule{}, - client: client, - VdcGroup: dfwRule.VdcGroup, - } - - rule.ID = dfwRule.Rule.ID - err = client.OpenApiPutItem(apiVersion, urlRef, nil, rule, returnObjectSingleRule.Rule, nil) - if err != nil { - return nil, fmt.Errorf("error updating Distributed Firewall rules: %s", err) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", dfwRule.Rule.ID}, + entityLabel: labelDistributedFirewallRule, } - - return returnObjectSingleRule, nil + outerType := DistributedFirewallRule{client: dfwRule.client, VdcGroup: dfwRule.VdcGroup} + return updateOuterEntity(dfwRule.client, outerType, c, rule) } // Delete a single Distributed Firewall Rule func (dfwRule *DistributedFirewallRule) Delete() error { - if dfwRule.Rule.ID == "" { - return fmt.Errorf("cannot delete NSX-T Distribute Firewall Rule without ID") - } - - client := dfwRule.client - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err - } - - // "default" policy is hardcoded because there is no other policy supported - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", dfwRule.Rule.ID) - if err != nil { - return err - } - - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - if err != nil { - return fmt.Errorf("error deleting NSX-T Distribute Firewall Rule with ID '%s': %s", dfwRule.Rule.ID, err) + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", dfwRule.Rule.ID}, } - - return nil + return deleteEntityById(dfwRule.client, c) } // getFirewallRuleIndexById searches for 'firewallRuleId' going through a list of available firewall diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway.go index 01604004d..88c59864f 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway.go @@ -183,6 +183,11 @@ func (adminOrg *AdminOrg) CreateNsxtEdgeGateway(edgeGatewayConfig *types.OpenAPI return nil, fmt.Errorf("error creating Edge Gateway: %s", err) } + err = returnEgw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplinks after update operation: %s", err) + } + return returnEgw, nil } @@ -197,6 +202,12 @@ func (egw *NsxtEdgeGateway) Refresh() error { return fmt.Errorf("error refreshing NSX-T Edge Gateway: %s", err) } egw.EdgeGateway = refreshedEdge.EdgeGateway + + err = egw.reorderUplinks() + if err != nil { + return fmt.Errorf("error reordering Edge Gateway Uplinks after refresh operation: %s", err) + } + return nil } @@ -231,6 +242,11 @@ func (egw *NsxtEdgeGateway) Update(edgeGatewayConfig *types.OpenAPIEdgeGateway) return nil, fmt.Errorf("error updating Edge Gateway: %s", err) } + err = egw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplinks after update operation: %s", err) + } + return returnEgw, nil } @@ -280,6 +296,30 @@ func (egw *NsxtEdgeGateway) MoveToVdcOrVdcGroup(vdcOrVdcGroupId string) (*NsxtEd return egw.Update(edgeGatewayConfig) } +// reorderUplinks will ensure that uplink at slice index 0 is the one backed by NSX-T Tier0 External network. +// NSX-T Edge Gateway can have many uplinks of different types (they are differentiated by 'backingType' field): +// * MANDATORY - exactly 1 uplink to Tier0 Gateway (External network backed by NSX-T T0 Gateway or NSX-T T0 Gateway VRF) [backingType==NSXT_TIER0 or NSXT_VRF_TIER0] +// * OPTIONAL - one or more External Network Uplinks (backed by NSX-T Segment backed External networks) [backingType==IMPORTED_T_LOGICAL_SWITCH] +// It is expected that the Tier0 gateway uplink is always at index 0, but we have seen where VCD API +// shuffles response values therefore it is important to ensure that uplink with +// backingType==NSXT_TIER0 or backingType==NSXT_VRF_TIER0 the element 0 in types.EdgeGatewayUplinks to avoid breaking functionality +// in upstream code. +// +// Note. This function wil be a noop in 10.4.0, because `backingType` was not present. However, this +// poses no risks because the can be only 1 uplink up to 10.4.1, when `backingType` was introduced. +func (egw *NsxtEdgeGateway) reorderUplinks() error { + if egw == nil || egw.EdgeGateway == nil { + return fmt.Errorf("edge gateway cannot be nil ") + } + + if len(egw.EdgeGateway.EdgeGatewayUplinks) == 0 { + return fmt.Errorf("no uplinks present in Edge Gateway") + } + + egw.EdgeGateway.EdgeGatewayUplinks = reorderEdgeGatewayUplinks(egw.EdgeGateway.EdgeGatewayUplinks) + return nil +} + // getNsxtEdgeGatewayById is a private parent for wrapped functions: // func (adminOrg *AdminOrg) GetNsxtEdgeGatewayByName(id string) (*NsxtEdgeGateway, error) // func (org *Org) GetNsxtEdgeGatewayByName(id string) (*NsxtEdgeGateway, error) @@ -315,6 +355,11 @@ func getNsxtEdgeGatewayById(client *Client, id string, queryParameters url.Value ErrorEntityNotFound, egw.EdgeGateway.GatewayBacking.GatewayType) } + err = egw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplink after API retrieval") + } + return egw, nil } @@ -365,6 +410,15 @@ func getAllNsxtEdgeGateways(client *Client, queryParameters url.Values) ([]*Nsxt onlyNsxtEdges := filterOnlyNsxtEdges(wrappedResponses) + // Reorder uplink in all Edge Gateways + for edgeIndex := range onlyNsxtEdges { + err := onlyNsxtEdges[edgeIndex].reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering NSX-T Edge Gateway Uplinks for gateway '%s' ('%s'): %s", + onlyNsxtEdges[edgeIndex].EdgeGateway.Name, onlyNsxtEdges[edgeIndex].EdgeGateway.ID, err) + } + } + return onlyNsxtEdges, nil } @@ -496,6 +550,66 @@ func (egw *NsxtEdgeGateway) GetAllocatedIpCount(refresh bool) (int, error) { return allocatedIpCount, nil } +// GetPrimaryNetworkAllocatedIpCount returns total count of allocated IPs for first NSX-T Edge +// Gateway uplink +func (egw *NsxtEdgeGateway) GetPrimaryNetworkAllocatedIpCount(refresh bool) (int, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + + allocatedIpCount := 0 + + for _, subnet := range egw.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values { + if subnet.TotalIPCount != nil { + allocatedIpCount += *subnet.TotalIPCount + } + } + + return allocatedIpCount, nil +} + +// GetAllocatedIpCountByUplinkType will return a sum of allocated IPs for particular `uplinkType` +// `uplinkType` can be one of 'NSXT_TIER0', 'NSXT_VRF_TIER0', 'IMPORTED_T_LOGICAL_SWITCH' +// +// Note. This function is based on BackingType field and requires at least VCD 10.4.1 +func (egw *NsxtEdgeGateway) GetAllocatedIpCountByUplinkType(refresh bool, uplinkType string) (int, error) { + if egw.client.APIVCDMaxVersionIs("< 37.1") { + return 0, fmt.Errorf("this function requires at least VCD 10.4.1 to work") + } + + if uplinkType != "NSXT_TIER0" && + uplinkType != "IMPORTED_T_LOGICAL_SWITCH" && + uplinkType != "NSXT_VRF_TIER0" { + return 0, fmt.Errorf("invalid 'uplinkType', expected 'NSXT_TIER0', 'IMPORTED_T_LOGICAL_SWITCH' or 'NSXT_VRF_TIER0', got: %s", uplinkType) + } + + if refresh { + err := egw.Refresh() + if err != nil { + return 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + + allocatedIpCount := 0 + + for _, uplink := range egw.EdgeGateway.EdgeGatewayUplinks { + // counting IPs only for specific uplink type + if uplink.BackingType != nil && *uplink.BackingType != uplinkType { + continue + } + for _, subnet := range uplink.Subnets.Values { + if subnet.TotalIPCount != nil { + allocatedIpCount += *subnet.TotalIPCount + } + } + } + + return allocatedIpCount, nil +} + // GetUsedIpAddressSlice retrieves a list of used IP addresses in an Edge Gateway and returns it // using native Go type '[]netip.Addr' func (egw *NsxtEdgeGateway) GetUsedIpAddressSlice(refresh bool) ([]netip.Addr, error) { @@ -971,3 +1085,28 @@ func flattenGatewayUsedIpAddressesToIpSlice(usedIpAddresses []*types.GatewayUsed return usedIpSlice, nil } + +func reorderEdgeGatewayUplinks(edgeGatewayUplinks []types.EdgeGatewayUplinks) []types.EdgeGatewayUplinks { + // If only 1 uplink is present - there is nothing to reorder, because only mandatory uplink is present + if len(edgeGatewayUplinks) == 1 { + return edgeGatewayUplinks + } + + // Element 0 is External Network backed by Tier 0 Gateway or T0 Gateway VRF - nothing to do + if edgeGatewayUplinks[0].BackingType != nil && (*edgeGatewayUplinks[0].BackingType == "NSXT_TIER0" || *edgeGatewayUplinks[0].BackingType == "NSXT_VRF_TIER0") { + return edgeGatewayUplinks + } + + for uplinkIndex := range edgeGatewayUplinks { + if edgeGatewayUplinks[uplinkIndex].BackingType != nil && (*edgeGatewayUplinks[uplinkIndex].BackingType == "NSXT_TIER0" || *edgeGatewayUplinks[uplinkIndex].BackingType == "NSXT_VRF_TIER0") { + // Swap elements so that 'NSXT_TIER0' or 'NSXT_VRF_TIER0' is at position 0 + t0BackedUplink := edgeGatewayUplinks[uplinkIndex] + edgeGatewayUplinks[uplinkIndex] = edgeGatewayUplinks[0] + edgeGatewayUplinks[0] = t0BackedUplink + + return edgeGatewayUplinks + } + } + + return edgeGatewayUplinks +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway_dns.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway_dns.go new file mode 100644 index 000000000..25c50d9c6 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_edgegateway_dns.go @@ -0,0 +1,115 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtEdgeGatewayDns can be used to configure DNS on NSX-T Edge Gateway. +type NsxtEdgeGatewayDns struct { + NsxtEdgeGatewayDns *types.NsxtEdgeGatewayDns + client *Client + EdgeGatewayId string +} + +// GetDnsConfig retrieves the DNS configuration for the underlying edge gateway +func (egw *NsxtEdgeGateway) GetDnsConfig() (*NsxtEdgeGatewayDns, error) { + return getDnsConfig(egw.client, egw.EdgeGateway.ID) +} + +// UpdateDnsConfig updates the DNS configuration for the Edge Gateway +func (egw *NsxtEdgeGateway) UpdateDnsConfig(updatedConfig *types.NsxtEdgeGatewayDns) (*NsxtEdgeGatewayDns, error) { + return updateDnsConfig(updatedConfig, egw.client, egw.EdgeGateway.ID) +} + +// Update updates the DNS configuration for the underlying Edge Gateway +func (dns *NsxtEdgeGatewayDns) Update(updatedConfig *types.NsxtEdgeGatewayDns) (*NsxtEdgeGatewayDns, error) { + return updateDnsConfig(updatedConfig, dns.client, dns.EdgeGatewayId) +} + +// Refresh refreshes the DNS configuration for the Edge Gateway +func (dns *NsxtEdgeGatewayDns) Refresh() error { + updatedDns, err := getDnsConfig(dns.client, dns.EdgeGatewayId) + if err != nil { + return err + } + dns.NsxtEdgeGatewayDns = updatedDns.NsxtEdgeGatewayDns + + return nil +} + +// Delete deletes the DNS configuration for the Edge Gateway +func (dns *NsxtEdgeGatewayDns) Delete() error { + client := dns.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dns.EdgeGatewayId)) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + +func getDnsConfig(client *Client, edgeGatewayId string) (*NsxtEdgeGatewayDns, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + dnsConfig := &NsxtEdgeGatewayDns{ + client: client, + EdgeGatewayId: edgeGatewayId, + } + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &dnsConfig.NsxtEdgeGatewayDns, nil) + if err != nil { + return nil, err + } + + return dnsConfig, nil + +} + +func updateDnsConfig(updatedConfig *types.NsxtEdgeGatewayDns, client *Client, edgeGatewayId string) (*NsxtEdgeGatewayDns, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + dns := &NsxtEdgeGatewayDns{ + client: client, + EdgeGatewayId: edgeGatewayId, + } + err = client.OpenApiPutItem(apiVersion, urlRef, nil, updatedConfig, &dns.NsxtEdgeGatewayDns, nil) + if err != nil { + return nil, err + } + + return dns, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_l2_vpn_tunnel.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_l2_vpn_tunnel.go new file mode 100644 index 000000000..3e3591420 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_l2_vpn_tunnel.go @@ -0,0 +1,283 @@ +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtL2VpnTunnel extends an organization VDC by enabling virtual machines to +// maintain their network connectivity across geographical boundaries while keeping +// the same IP address. The connection is secured with a route-based IPSec tunnel between the two sides of the tunnel. +// The L2 VPN service can be configured on an NSX-T edge gateway in a VMware Cloud Director environment +// to create a L2 VPN tunnel. Virtual machines remain on the same subnet, which extends +// the organization VDC by stretching its network. This way, an edge gateway at one site can provide +// all services to virtual machines on the other site. +type NsxtL2VpnTunnel struct { + NsxtL2VpnTunnel *types.NsxtL2VpnTunnel + client *Client + // edgeGatewayId is stored for usage in NsxtFirewall receiver functions + edgeGatewayId string +} + +// CreateL2VpnTunnel creates a L2 VPN Tunnel on the provided NSX-T Edge Gateway and returns +// the tunnel +func (egw *NsxtEdgeGateway) CreateL2VpnTunnel(tunnel *types.NsxtL2VpnTunnel) (*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot create L2 VPN tunnel for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // When creating a L2 VPN tunnel, its ID is stored in the creation task Details section, + // so we need to fetch the newly created tunnel manually + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, tunnel) + if err != nil { + return nil, fmt.Errorf("error creating L2 VPN tunnel: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting for L2 VPN tunnel to be created: %s", err) + } + + newTunnel, err := egw.GetL2VpnTunnelById(task.Task.Details) + if err != nil { + return nil, fmt.Errorf("error getting L2 VPN tunnel with id %s: %s", task.Task.Details, err) + } + + return newTunnel, nil +} + +// Refresh updates the provided NsxtL2VpnTunnel and returns an error if it failed +func (l2Vpn *NsxtL2VpnTunnel) Refresh() error { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return err + } + + refreshedTunnel := &types.NsxtL2VpnTunnel{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &refreshedTunnel, nil) + if err != nil { + return err + } + l2Vpn.NsxtL2VpnTunnel = refreshedTunnel + + return nil +} + +// GetAllL2VpnTunnels fetches all L2 VPN tunnels that are created on the Edge Gateway. +func (egw *NsxtEdgeGateway) GetAllL2VpnTunnels(queryParameters url.Values) ([]*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get L2 VPN tunnels for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtL2VpnTunnel{{}} + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtL2VpnTunnel types with client + results := make([]*NsxtL2VpnTunnel, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &NsxtL2VpnTunnel{ + NsxtL2VpnTunnel: typeResponses[sliceIndex], + edgeGatewayId: egw.EdgeGateway.ID, + client: egw.client, + } + } + + return results, nil +} + +// GetL2VpnTunnelByName gets the L2 VPN Tunnel by name +func (egw *NsxtEdgeGateway) GetL2VpnTunnelByName(name string) (*NsxtL2VpnTunnel, error) { + results, err := egw.GetAllL2VpnTunnels(nil) + if err != nil { + return nil, err + } + + foundTunnels := make([]*NsxtL2VpnTunnel, 0) + for _, tunnel := range results { + if tunnel.NsxtL2VpnTunnel.Name == name { + foundTunnels = append(foundTunnels, tunnel) + } + } + + return oneOrError("name", name, foundTunnels) +} + +// GetL2VpnTunnelById gets the L2 VPN Tunnel by its ID +func (egw *NsxtEdgeGateway) GetL2VpnTunnelById(id string) (*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get L2 VPN tunnel for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + tunnel := &NsxtL2VpnTunnel{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + } + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &tunnel.NsxtL2VpnTunnel, nil) + if err != nil { + return nil, err + } + + return tunnel, nil +} + +// Statistics retrieves connection statistics for a given L2 VPN Tunnel configured on an Edge Gateway. +func (l2Vpn *NsxtL2VpnTunnel) Statistics() (*types.EdgeL2VpnTunnelStatistics, error) { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnelStatistics + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId, l2Vpn.NsxtL2VpnTunnel.ID)) + if err != nil { + return nil, err + } + + statistics := &types.EdgeL2VpnTunnelStatistics{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &statistics, nil) + if err != nil { + return nil, err + } + + return statistics, nil +} + +// Status retrieves status of a given L2 VPN Tunnel. +func (l2Vpn *NsxtL2VpnTunnel) Status() (*types.EdgeL2VpnTunnelStatus, error) { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnelStatus + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId, l2Vpn.NsxtL2VpnTunnel.ID)) + if err != nil { + return nil, err + } + + status := &types.EdgeL2VpnTunnelStatus{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &status, nil) + if err != nil { + return nil, err + } + + return status, nil +} + +// Update updates the L2 VPN tunnel with the provided parameters as the argument +func (l2Vpn *NsxtL2VpnTunnel) Update(tunnelParams *types.NsxtL2VpnTunnel) (*NsxtL2VpnTunnel, error) { + if l2Vpn.NsxtL2VpnTunnel.SessionMode != tunnelParams.SessionMode { + return nil, fmt.Errorf("error updating the L2 VPN Tunnel: session mode can't be changed after creation") + } + + if tunnelParams.SessionMode == "CLIENT" && !tunnelParams.Enabled { + // There is a known bug up to 10.5.0, the CLIENT sessions can't be + // disabled and can result in unexpected behaviour for the following + // operations + if l2Vpn.client.APIVCDMaxVersionIs("<= 38.0") { + return nil, fmt.Errorf("client sessions can't be disabled on VCD versions up to 10.5.0") + } + } + + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return nil, err + } + + tunnelParams.Version.Version = l2Vpn.NsxtL2VpnTunnel.Version.Version + + newTunnel := &NsxtL2VpnTunnel{ + client: l2Vpn.client, + edgeGatewayId: l2Vpn.edgeGatewayId, + } + err = client.OpenApiPutItem(apiVersion, urlRef, nil, tunnelParams, &newTunnel.NsxtL2VpnTunnel, nil) + if err != nil { + return nil, err + } + + return newTunnel, nil +} + +// Delete deletes the L2 VPN Tunnel +// On versions up to 10.5.0 (as of writing) there is a bug with deleting +// CLIENT tunnels. If there are any networks attached to the tunnel, the +// DELETE call will fail the amount of times the resource was updated, +// so the best choice is to remove the networks and then call Delete(), or +// call Delete() in a loop until it's successful. +func (l2Vpn *NsxtL2VpnTunnel) Delete() error { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_manager.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_manager.go new file mode 100644 index 000000000..722bf31fc --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_manager.go @@ -0,0 +1,69 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +type NsxtManager struct { + NsxtManager *types.NsxtManager + VCDClient *VCDClient +} + +// GetNsxtManagerByName searches for NSX-T managers available in VCD and returns the one that +// matches name +func (vcdClient *VCDClient) GetNsxtManagerByName(name string) (*NsxtManager, error) { + nsxtManagers, err := vcdClient.QueryNsxtManagerByName(name) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Manager by name '%s': %s", name, err) + } + + // Double check that exactly one NSX-T Manager is found and throw error otherwise + singleNsxtManager, err := oneOrError("name", name, nsxtManagers) + if err != nil { + return nil, err + } + + resp, err := vcdClient.Client.executeJsonRequest(singleNsxtManager.HREF, http.MethodGet, nil, "error retrieving NSX-T Manager: %s") + if err != nil { + return nil, err + } + + defer closeBody(resp) + + nsxtManager := NsxtManager{ + NsxtManager: &types.NsxtManager{}, + VCDClient: vcdClient, + } + + err = decodeBody(types.BodyTypeJSON, resp, nsxtManager.NsxtManager) + if err != nil { + return nil, err + } + + return &nsxtManager, nil +} + +// Urn ensures that a URN is returned insted of plain UUID because VCD returns UUID, but requires +// URN in other APIs quite often. +func (nsxtManager *NsxtManager) Urn() (string, error) { + if nsxtManager == nil || nsxtManager.NsxtManager == nil || nsxtManager.NsxtManager.ID == "" { + return "", fmt.Errorf("NSX-T manager structure is incomplete - cannot build URN without ID") + } + + if isUrn(nsxtManager.NsxtManager.ID) { + return nsxtManager.NsxtManager.ID, nil + } + + nsxtManagerUrn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", nsxtManager.NsxtManager.ID) + if err != nil { + return "", fmt.Errorf("error building NSX-T Manager URN from ID '%s': %s", nsxtManager.NsxtManager.ID, err) + } + return nsxtManagerUrn, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profile_template.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profile_template.go new file mode 100644 index 000000000..c45c7e6ed --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profile_template.go @@ -0,0 +1,102 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const labelNsxtSegmentProfileTemplate = "NSX-T Segment Profile Template" + +// NsxtSegmentProfileTemplate contains a structure for configuring Segment Profile Templates +type NsxtSegmentProfileTemplate struct { + NsxtSegmentProfileTemplate *types.NsxtSegmentProfileTemplate + VCDClient *VCDClient +} + +// wrap is a hidden helper that facilitates the usage of a generic CRUD function +// +//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck +func (n NsxtSegmentProfileTemplate) wrap(inner *types.NsxtSegmentProfileTemplate) *NsxtSegmentProfileTemplate { + n.NsxtSegmentProfileTemplate = inner + return &n +} + +// CreateSegmentProfileTemplate creates a Segment Profile Template that can later be assigned to +// global VCD configuration, Org VDC or Org VDC Network +func (vcdClient *VCDClient) CreateSegmentProfileTemplate(segmentProfileConfig *types.NsxtSegmentProfileTemplate) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + entityLabel: labelNsxtSegmentProfileTemplate, + } + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, segmentProfileConfig) +} + +// GetAllSegmentProfileTemplates retrieves all Segment Profile Templates +func (vcdClient *VCDClient) GetAllSegmentProfileTemplates(queryFilter url.Values) ([]*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + entityLabel: labelNsxtSegmentProfileTemplate, + queryParameters: queryFilter, + } + + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return getAllOuterEntities[NsxtSegmentProfileTemplate, types.NsxtSegmentProfileTemplate](&vcdClient.Client, outerType, c) +} + +// GetSegmentProfileTemplateById retrieves Segment Profile Template by ID +func (vcdClient *VCDClient) GetSegmentProfileTemplateById(id string) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{id}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return getOuterEntity[NsxtSegmentProfileTemplate, types.NsxtSegmentProfileTemplate](&vcdClient.Client, outerType, c) +} + +// GetSegmentProfileTemplateByName retrieves Segment Profile Template by ID +func (vcdClient *VCDClient) GetSegmentProfileTemplateByName(name string) (*NsxtSegmentProfileTemplate, error) { + filterByName := copyOrNewUrlValues(nil) + filterByName = queryParameterFilterAnd(fmt.Sprintf("name==%s", name), filterByName) + + allSegmentProfileTemplates, err := vcdClient.GetAllSegmentProfileTemplates(filterByName) + if err != nil { + return nil, err + } + + singleSegmentProfileTemplate, err := oneOrError("name", name, allSegmentProfileTemplates) + if err != nil { + return nil, err + } + + return singleSegmentProfileTemplate, nil +} + +// Update Segment Profile Template +func (spt *NsxtSegmentProfileTemplate) Update(nsxtSegmentProfileTemplateConfig *types.NsxtSegmentProfileTemplate) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{nsxtSegmentProfileTemplateConfig.ID}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + outerType := NsxtSegmentProfileTemplate{VCDClient: spt.VCDClient} + return updateOuterEntity(&spt.VCDClient.Client, outerType, c, nsxtSegmentProfileTemplateConfig) +} + +// Delete allows deleting NSX-T Segment Profile Template +func (spt *NsxtSegmentProfileTemplate) Delete() error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{spt.NsxtSegmentProfileTemplate.ID}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + return deleteEntityById(&spt.VCDClient.Client, c) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profiles.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profiles.go new file mode 100644 index 000000000..848ebf64b --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_segment_profiles.go @@ -0,0 +1,129 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const ( + labelIpDiscoveryProfiles = "IP Discovery Profiles" + labelMacDiscoveryProfiles = "MAC Discovery Profiles" + labelSpoofGuardProfiles = "Spoof Guard Profiles" + labelQosProfiles = "QoS Profiles" + labelSegmentSecurityProfiles = "Segment Security Profiles" +) + +// GetAllIpDiscoveryProfiles retrieves all IP Discovery Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllIpDiscoveryProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileIpDiscovery, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles, + queryParameters: queryParameters, + entityLabel: labelIpDiscoveryProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileIpDiscovery](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetIpDiscoveryProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileIpDiscovery, error) { + apiFilteredEntities, err := vcdClient.GetAllIpDiscoveryProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelIpDiscoveryProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllMacDiscoveryProfiles retrieves all MAC Discovery Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllMacDiscoveryProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileMacDiscovery, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentMacDiscoveryProfiles, + queryParameters: queryParameters, + entityLabel: labelMacDiscoveryProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileMacDiscovery](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetMacDiscoveryProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileMacDiscovery, error) { + apiFilteredEntities, err := vcdClient.GetAllMacDiscoveryProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelMacDiscoveryProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllSpoofGuardProfiles retrieves all Spoof Guard Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllSpoofGuardProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentSpoofGuard, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSpoofGuardProfiles, + queryParameters: queryParameters, + entityLabel: labelSpoofGuardProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentSpoofGuard](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetSpoofGuardProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentSpoofGuard, error) { + apiFilteredEntities, err := vcdClient.GetAllSpoofGuardProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelSpoofGuardProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllQoSProfiles retrieves all QoS Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllQoSProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentQosProfile, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentQosProfiles, + queryParameters: queryParameters, + entityLabel: labelQosProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentQosProfile](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetQoSProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentQosProfile, error) { + apiFilteredEntities, err := vcdClient.GetAllQoSProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelQosProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllSegmentSecurityProfiles retrieves all Segment Security Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllSegmentSecurityProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentSecurity, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSecurityProfiles, + queryParameters: queryParameters, + entityLabel: labelSegmentSecurityProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentSecurity](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetSegmentSecurityProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentSecurity, error) { + apiFilteredEntities, err := vcdClient.GetAllSegmentSecurityProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelSegmentSecurityProfiles, apiFilteredEntities, "DisplayName", name) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_transport_zones.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_transport_zones.go new file mode 100644 index 000000000..76922d9d4 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/nsxt_transport_zones.go @@ -0,0 +1,48 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +func (vcdClient *VCDClient) GetAllNsxtTransportZones(nsxtManagerId string, queryParameters url.Values) ([]*types.TransportZone, error) { + if nsxtManagerId == "" { + return nil, fmt.Errorf("empty NSX-T manager ID") + } + + if !isUrn(nsxtManagerId) { + return nil, fmt.Errorf("NSX-T manager ID is not URN (e.g. 'urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", nsxtManagerId) + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTransportZones + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + filterField := "_context" + if client.APIClientVersionIs(">=38.0") { + // field "networkProviderId" does not exist prior to API 38.0, where field "_context" is deprecated + filterField = "networkProviderId" + } + queryParams = queryParameterFilterAnd(fmt.Sprintf("%s==%s", filterField, nsxtManagerId), queryParams) + var typeResponses []*types.TransportZone + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_endpoints.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_endpoints.go index 1ccc8739a..31ccb14d6 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_endpoints.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_endpoints.go @@ -38,6 +38,8 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointQosProfiles: "36.2", // VCD 10.3.2+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayQos: "36.2", // VCD 10.3.2+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDhcpForwarder: "36.1", // VCD 10.3.1+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns: "37.0", // VCD 10.4.0+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel: "35.0", // VCD 10.2.0+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewaySlaacProfile: "35.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes: "37.0", // VCD 10.4.0+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways: "34.0", @@ -48,6 +50,7 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules: "34.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules: "34.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles: "37.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings: "36.1", // VCD 10.3.1+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcCapabilities: "32.0", @@ -73,12 +76,13 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesBehaviorsInvocations: "35.0", // VCD 10.2+ // IP Spaces - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: "37.1", // VCD 10.4.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries: "37.1", // VCD 10.4.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks: "37.1", // VCD 10.4.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate: "37.1", // VCD 10.4.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations: "37.1", // VCD 10.4.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceFloatingIpSuggestions: "37.1", // VCD 10.4.1+ // NSX-T ALB (Advanced/AVI Load Balancer) support was introduced in 10.2 types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController: "35.0", // VCD 10.2+ @@ -105,13 +109,20 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies: "35.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile: "36.0", // VCD 10.3+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVirtualCenters: "36.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePools: "36.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolsBrowseAll: "36.2", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolHardware: "36.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools: "36.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPoolSummaries: "36.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointStorageProfiles: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVirtualCenters: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePools: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolsBrowseAll: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolHardware: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPoolSummaries: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointStorageProfiles: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentMacDiscoveryProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSpoofGuardProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentQosProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSecurityProfiles: "36.2", // Extensions API endpoints. These are not versioned types.OpenApiEndpointExtensionsUi: "35.0", // VCD 10.2+ @@ -123,9 +134,14 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiEndpointExtensionsUiTenantsUnpublish: "35.0", // VCD 10.2+ // Endpoints for managing tokens and service accounts - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens: "36.1", // VCD 10.3.1+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts: "37.0", // VCD 10.4.0+ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccountGrant: "37.0", // VCD 10.4.0+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens: "36.1", // VCD 10.3.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts: "37.0", // VCD 10.4.0+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccountGrant: "37.0", // VCD 10.4.0+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTransportZones: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVCenterDistributedSwitch: "33.0", + + // Endpoint for managing vGPU profiles + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile: "36.2", } // endpointElevatedApiVersions endpoint elevated API versions @@ -194,10 +210,18 @@ var endpointElevatedApiVersions = map[string][]string{ //"35.0", // Introduced support "37.1", // Exposes computed field `UsingIpSpace` in `types.EdgeGatewayUplinks` }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns: { + "37.0", // Introduced support + "38.0", // New field SnatRuleExternalIpAddress + }, types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate: { //"37.1", // Introduced support "37.2", // Adds 'value' field }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: { + //"37.1", // Introduced support + "38.0", // Adds 'DefaultGatewayServiceConfig' structure for firewall and NAT rule creation + }, } // checkOpenApiEndpointCompatibility checks if VCD version (to which the client is connected) is sufficient to work with diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_inner_entities.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_inner_entities.go new file mode 100644 index 000000000..81aeb02df --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_inner_entities.go @@ -0,0 +1,288 @@ +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +// crudConfig contains configuration that must be supplied when invoking generic functions that are defined +// in `openapi_generic_inner_entities.go` and `openapi_generic_outer_entities.go` +type crudConfig struct { + // Mandatory parameters + + // entityLabel contains friendly entity name that is used for logging meaningful errors + entityLabel string + + // endpoint in the usual format (e.g. types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles) + endpoint string + + // Optional parameters + + // endpointParams contains a slice of strings that will be used to construct the request URL. It will + // initially replace '%s' placeholders in the `endpoint` (if any) and will add them as suffix + // afterwards + endpointParams []string + + // queryParameters will be passed as GET queries to the URL. Usually they are used for API filtering parameters + queryParameters url.Values + // additionalHeader can be used to pass additional headers for API calls. One of the common purposes is to pass + // tenant context + additionalHeader map[string]string +} + +// validate should catch errors in consuming generic CRUD functions and should never produce false +// positives. +func (c crudConfig) validate() error { + // crudConfig misconfiguration - we can panic so that developer catches the problem during + // development of this SDK + if c.entityLabel == "" { + panic("'entityName' must always be specified when initializing crudConfig") + } + + if c.endpoint == "" { + panic("'endpoint' must always be specified when initializing crudConfig") + } + + // softer validations that consumers of this SDK can manipulate + + // If `endpointParams` is specified in `crudConfig` (it is not nil), then it must contain at + // least a single non empty string parameter + // Such validation should prevent cases where some ID is not speficied upon function call. + // E.g.: endpointParams: []string{vdcId}, <--- vdcId comes from consumer of the SDK + // If the user specified empty `vdcId` - we'd validate this + for _, paramValue := range c.endpointParams { + if paramValue == "" { + return fmt.Errorf(`endpointParams were specified but they contain empty value "" for %s. %#v`, + c.entityLabel, c.endpointParams) + } + } + + return nil +} + +// createInnerEntity implements a common pattern for creating an entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func createInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) { + if err := c.validate(); err != nil { + return nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, fmt.Errorf("error getting API version for creating entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, fmt.Errorf("error building API endpoint for entity '%s' creation: %s", c.entityLabel, err) + } + + createdInnerEntityConfig := new(I) + err = client.OpenApiPostItem(apiVersion, urlRef, c.queryParameters, innerConfig, createdInnerEntityConfig, c.additionalHeader) + if err != nil { + return nil, fmt.Errorf("error creating entity of type '%s': %s", c.entityLabel, err) + } + + return createdInnerEntityConfig, nil +} + +// updateInnerEntity implements a common pattern for updating entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func updateInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) { + // Discarding returned headers to better match return signature for most common cases + updatedInnerEntity, _, err := updateInnerEntityWithHeaders(client, c, innerConfig) + return updatedInnerEntity, err +} + +// updateInnerEntityWithHeaders implements a common pattern for updating entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func updateInnerEntityWithHeaders[I any](client *Client, c crudConfig, innerConfig *I) (*I, http.Header, error) { + if err := c.validate(); err != nil { + return nil, nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, nil, fmt.Errorf("error getting API version for updating entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, nil, fmt.Errorf("error building API endpoint for entity '%s' update: %s", c.entityLabel, err) + } + + updatedInnerEntityConfig := new(I) + headers, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, c.queryParameters, innerConfig, updatedInnerEntityConfig, c.additionalHeader) + if err != nil { + return nil, nil, fmt.Errorf("error updating entity of type '%s': %s", c.entityLabel, err) + } + + return updatedInnerEntityConfig, headers, nil +} + +// getInnerEntity is an implementation for a common pattern in our code where we have to retrieve +// outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity. +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getInnerEntity[I any](client *Client, c crudConfig) (*I, error) { + // Discarding returned headers to better match return signature for most common cases + innerEntity, _, err := getInnerEntityWithHeaders[I](client, c) + return innerEntity, err +} + +// getInnerEntityWithHeaders is an implementation for a common pattern in our code where we have to retrieve +// outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity. +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getInnerEntityWithHeaders[I any](client *Client, c crudConfig) (*I, http.Header, error) { + if err := c.validate(); err != nil { + return nil, nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err) + } + + typeResponse := new(I) + headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, c.queryParameters, typeResponse, c.additionalHeader) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving entity of type '%s': %s", c.entityLabel, err) + } + + return typeResponse, headers, nil +} + +// getAllInnerEntities can be used to retrieve a slice of any inner entities in the OpenAPI +// endpoints that are not nested in outer types +// +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getAllInnerEntities[I any](client *Client, c crudConfig) ([]*I, error) { + if err := c.validate(); err != nil { + return nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err) + } + + typeResponses := make([]*I, 0) + err = client.OpenApiGetAllItems(apiVersion, urlRef, c.queryParameters, &typeResponses, c.additionalHeader) + if err != nil { + return nil, fmt.Errorf("error retrieving all entities of type '%s': %s", c.entityLabel, err) + } + + return typeResponses, nil +} + +// deleteEntityById performs a common operation for OpenAPI endpoints that calls DELETE method for a +// given endpoint. +// Note. It does not use generics for the operation, but is held in this file with other CRUD entries +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func deleteEntityById(client *Client, c crudConfig) error { + if err := c.validate(); err != nil { + return err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return err + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, c.queryParameters, c.additionalHeader) + + if err != nil { + return fmt.Errorf("error deleting %s: %s", c.entityLabel, err) + } + + return nil +} + +func urlFromEndpoint(endpoint string, endpointParams []string) (string, error) { + // Count how many '%s' placeholders exist in the 'endpoint' + placeholderCount := strings.Count(endpoint, "%s") + + // Validation. At the very minimum all placeholders must have their replacements - otherwise it + // is an error as we never want to query an endpoint that still has placeholders '%s' + if len(endpointParams) < placeholderCount { + return "", fmt.Errorf("endpoint '%s' has unpopulated placeholders", endpoint) + } + + // if there are no 'endpointParams' - exit with the same endpoint + if len(endpointParams) == 0 { + return endpoint, nil + } + + // Loop over given endpointParams and replace placeholders at first. Afterwards - amend any + // additional parameters to the end of endpoint + for _, v := range endpointParams { + // If there are placeholders '%s' to replace in the endpoint itself - do it + if placeholderCount > 0 { + endpoint = strings.Replace(endpoint, "%s", v, 1) + placeholderCount = placeholderCount - 1 + continue + } + + endpoint = endpoint + v + } + + return endpoint, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_outer_entities.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_outer_entities.go new file mode 100644 index 000000000..d7a77bf0e --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_generic_outer_entities.go @@ -0,0 +1,83 @@ +package govcd + +import ( + "fmt" + "net/http" +) + +// Generic type explanations +// Common generic parameter names seen in this code +// O - Outer type that is in the `govcd` package. (e.g. 'IpSpace') +// I - Inner type the type that is being marshalled/unmarshalled (usually in `types` package. E.g. `types.IpSpace`) + +// outerEntityWrapper is a type constraint that outer entities must implement in order to +// use generic CRUD functions defined in this file +type outerEntityWrapper[O any, I any] interface { + // wrap is a value receiver function that must implement one thing for a concrete type - wrap + // pointer to innter entity I and return pointer to outer entity O + wrap(inner *I) *O +} + +// createOuterEntity creates an outer entity with given inner entity config +func createOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig, innerEntityConfig *I) (*O, error) { + if innerEntityConfig == nil { + return nil, fmt.Errorf("entity config '%s' cannot be empty for create operation", c.entityLabel) + } + + createdInnerEntity, err := createInnerEntity(client, c, innerEntityConfig) + if err != nil { + return nil, err + } + + return outerEntity.wrap(createdInnerEntity), nil +} + +// updateOuterEntity updates an outer entity with given inner entity config +func updateOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig, innerEntityConfig *I) (*O, error) { + if innerEntityConfig == nil { + return nil, fmt.Errorf("entity config '%s' cannot be empty for update operation", c.entityLabel) + } + + updatedInnerEntity, err := updateInnerEntity(client, c, innerEntityConfig) + if err != nil { + return nil, err + } + + return outerEntity.wrap(updatedInnerEntity), nil +} + +// getOuterEntity retrieves a single outer entity +func getOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) (*O, error) { + retrievedInnerEntity, err := getInnerEntity[I](client, c) + if err != nil { + return nil, err + } + + return outerEntity.wrap(retrievedInnerEntity), nil +} + +// getOuterEntity retrieves a single outer entity +func getOuterEntityWithHeaders[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) (*O, http.Header, error) { + retrievedInnerEntity, headers, err := getInnerEntityWithHeaders[I](client, c) + if err != nil { + return nil, nil, err + } + + return outerEntity.wrap(retrievedInnerEntity), headers, nil +} + +// getAllOuterEntities retrieves all outer entities +func getAllOuterEntities[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) ([]*O, error) { + retrievedAllInnerEntities, err := getAllInnerEntities[I](client, c) + if err != nil { + return nil, err + } + + wrappedOuterEntities := make([]*O, len(retrievedAllInnerEntities)) + for index, singleInnerEntity := range retrievedAllInnerEntities { + // outerEntity.wrap() is a value receiver, therefore it creates a shallow copy for each call + wrappedOuterEntities[index] = outerEntity.wrap(singleInnerEntity) + } + + return wrappedOuterEntities, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_org_network.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_org_network.go index 99185d37d..a057e8d27 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_org_network.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/openapi_org_network.go @@ -12,6 +12,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelOrgVdcNetworkSegmentProfile = "Org VDC Network Segment Profile" + // OpenApiOrgVdcNetwork uses OpenAPI endpoint to operate both - NSX-T and NSX-V Org VDC networks type OpenApiOrgVdcNetwork struct { OpenApiOrgVdcNetwork *types.OpenApiOrgVdcNetwork @@ -375,3 +377,23 @@ func createOpenApiOrgVdcNetwork(client *Client, OrgVdcNetworkConfig *types.OpenA return returnEgw, nil } + +// GetSegmentProfile retrieves Segment Profile configuration for a single Org VDC Network +func (orgVdcNet *OpenApiOrgVdcNetwork) GetSegmentProfile() (*types.OrgVdcNetworkSegmentProfiles, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles, + endpointParams: []string{orgVdcNet.OpenApiOrgVdcNetwork.ID}, + entityLabel: labelOrgVdcNetworkSegmentProfile, + } + return getInnerEntity[types.OrgVdcNetworkSegmentProfiles](orgVdcNet.client, c) +} + +// UpdateSegmentProfile updates a Segment Profile with a given configuration +func (orgVdcNet *OpenApiOrgVdcNetwork) UpdateSegmentProfile(entityConfig *types.OrgVdcNetworkSegmentProfiles) (*types.OrgVdcNetworkSegmentProfiles, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles, + endpointParams: []string{orgVdcNet.OpenApiOrgVdcNetwork.ID}, + entityLabel: labelOrgVdcNetworkSegmentProfile, + } + return updateInnerEntity(orgVdcNet.client, c, entityConfig) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go index cd1d1963d..ea1e19a3c 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/org.go @@ -130,8 +130,15 @@ func CreateCatalogWithStorageProfile(client *Client, links types.LinkList, Name, catalog := NewAdminCatalog(client) _, err := client.ExecuteRequest(createOrgLink.HREF, http.MethodPost, "application/vnd.vmware.admin.catalog+xml", "error creating catalog: %s", vcomp, catalog.AdminCatalog) + if err != nil { + return nil, err + } - return catalog, err + err = catalog.WaitForTasks() + if err != nil { + return nil, err + } + return catalog, nil } // CreateCatalog creates a catalog with given name and description under @@ -143,6 +150,17 @@ func (org *Org) CreateCatalog(name, description string) (Catalog, error) { if err != nil { return Catalog{}, err } + catalog.parent = org + + err = catalog.Refresh() + if err != nil { + return Catalog{}, err + } + // Make sure that the creation task is finished + err = catalog.WaitForTasks() + if err != nil { + return Catalog{}, err + } return *catalog, nil } @@ -582,3 +600,17 @@ func queryCatalogList(client *Client, filterFields map[string]string) ([]*types. util.Logger.Printf("[DEBUG] QueryCatalogList returned with : %#v and error: %s", catalogs, err) return catalogs, nil } + +// GetVappByHref returns a vApp reference by running a VCD API call +// If no valid vApp is found, it returns a nil VApp reference and an error +func (org *Org) GetVAppByHref(vappHref string) (*VApp, error) { + newVapp := NewVApp(org.client) + + _, err := org.client.ExecuteRequest(vappHref, http.MethodGet, + "", "error retrieving vApp: %s", nil, newVapp.VApp) + + if err != nil { + return nil, err + } + return newVapp, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/provider_vdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/provider_vdc.go index 2e572dbe0..10f580232 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/provider_vdc.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/provider_vdc.go @@ -2,12 +2,13 @@ package govcd import ( "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" - "github.com/vmware/go-vcloud-director/v2/util" "io" "net/http" "net/url" "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) // ProviderVdc is the basic Provider VDC structure, contains the minimum set of attributes. @@ -422,7 +423,7 @@ func (pvdc *ProviderVdcExtended) DeleteResourcePools(resourcePools []*ResourcePo return fmt.Errorf("resource pool %s not found in provider VDC %s", rp.ResourcePool.Name, pvdc.VMWProviderVdc.Name) } if foundUsed.IsPrimary { - return fmt.Errorf("resource pool %s (%s) caannot be removed, because it is the primary one for provider VDC %s", + return fmt.Errorf("resource pool %s (%s) can not be removed, because it is the primary one for provider VDC %s", rp.ResourcePool.Name, rp.ResourcePool.Moref, pvdc.VMWProviderVdc.Name) } if foundUsed.IsEnabled { @@ -462,11 +463,11 @@ func (pvdc *ProviderVdcExtended) DeleteResourcePools(resourcePools []*ResourcePo // GetResourcePools returns the Resource Pools belonging to this provider VDC func (pvdc *ProviderVdcExtended) GetResourcePools() ([]*types.QueryResultResourcePoolRecordType, error) { - resourcePools, err := pvdc.client.cumulativeQuery(types.QtResourcePool, map[string]string{ + resourcePools, err := pvdc.client.cumulativeQuery(types.QtResourcePool, nil, map[string]string{ "type": types.QtResourcePool, "filter": fmt.Sprintf("providerVdc==%s", url.QueryEscape(extractUuid(pvdc.VMWProviderVdc.HREF))), "filterEncoded": "true", - }, nil) + }) if err != nil { return nil, fmt.Errorf("could not get the Resource pool: %s", err) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/query_metadata.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/query_metadata.go index f031195e8..6086d20b8 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/query_metadata.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/query_metadata.go @@ -62,7 +62,7 @@ func queryFieldsOnDemand(queryType string) ([]string, error) { vmFields = []string{"catalogName", "container", "containerName", "datastoreName", "description", "gcStatus", "guestOs", "hardwareVersion", "hostName", "isAutoNature", "isDeleted", "isDeployed", "isPublished", "isVAppTemplate", "isVdcEnabled", "memoryMB", "moref", "name", "numberOfCpus", "org", "status", - "storageProfileName", "vc", "vdc", "vmToolsVersion", "containerStatus", "pvdcHighestSupportedHardwareVersion", + "storageProfileName", "vc", "vdc", "vdcName", "vmToolsVersion", "containerStatus", "pvdcHighestSupportedHardwareVersion", "isComputePolicyCompliant", "vmSizingPolicyId", "vmPlacementPolicyId", "encrypted", "dateCreated", "totalStorageAllocatedMb", "isExpired"} vappFields = []string{"creationDate", "isBusy", "isDeployed", "isEnabled", "isExpired", "isInMaintenanceMode", "isPublic", @@ -175,6 +175,12 @@ func addResults(queryType string, cumulativeResults, newResults Results) (Result case types.QtResourcePool: cumulativeResults.Results.ResourcePoolRecord = append(cumulativeResults.Results.ResourcePoolRecord, newResults.Results.ResourcePoolRecord...) size = len(newResults.Results.ResourcePoolRecord) + case types.QtVappNetwork: + cumulativeResults.Results.VappNetworkRecord = append(cumulativeResults.Results.VappNetworkRecord, newResults.Results.VappNetworkRecord...) + size = len(newResults.Results.VappNetworkRecord) + case types.QtAdminVappNetwork: + cumulativeResults.Results.AdminVappNetworkRecord = append(cumulativeResults.Results.AdminVappNetworkRecord, newResults.Results.AdminVappNetworkRecord...) + size = len(newResults.Results.AdminVappNetworkRecord) default: return Results{}, 0, fmt.Errorf("query type %s not supported", queryType) @@ -212,6 +218,8 @@ func (client *Client) cumulativeQueryWithHeaders(queryType string, params, notEn types.QtResourcePool, types.QtNetworkPool, types.QtProviderVdcStorageProfile, + types.QtVappNetwork, + types.QtAdminVappNetwork, } // Make sure the query type is supported // We need to check early, as queries that would return less than 25 items (default page size) would succeed, @@ -227,6 +235,13 @@ func (client *Client) cumulativeQueryWithHeaders(queryType string, params, notEn return Results{}, fmt.Errorf("[cumulativeQuery] query type %s not supported", queryType) } + if params == nil { + params = make(map[string]string) + } + if len(notEncodedParams) == 0 { + notEncodedParams = map[string]string{"type": queryType} + } + result, err := client.QueryWithNotEncodedParamsWithHeaders(params, notEncodedParams, headers) if err != nil { return Results{}, err diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/sample_govcd_test_config.yaml b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/sample_govcd_test_config.yaml index 25d2c1dc0..f54375db4 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/sample_govcd_test_config.yaml +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/sample_govcd_test_config.yaml @@ -86,7 +86,7 @@ vcd: # NSX-T tier-0 VRF router used for external network tests tier0routerVrf: tier-0-router-vrf # Gateway QoS Profile used for NSX-T Edge Gateway Rate Limiting (defined in NSX-T Manager) - gatewayQosProfile: Gateway QoS Profile 1 + gatewayQosProfile: Gateway QoS Profile 1 # Existing External Network with correct configuration externalNetwork: tier0-backed-external-network # Existing NSX-T based VDC @@ -97,18 +97,30 @@ vcd: edgeGateway: nsxt-gw-name # Existing NSX-T segment to test NSX-T Imported Org Vdc network nsxtImportSegment: vcd-org-vdc-imported-network-backing + # Existing NSX-T segment to test Edge Gateway Uplinks + nsxtImportSegment2: vcd-org-vdc-imported-network-backing2 # Existing NSX-T Edge Cluster name nsxtEdgeCluster: existing-nsxt-edge-cluster - # AVI Controller URL + # AVI Controller URL nsxtAlbControllerUrl: https://unknown-hostname.com # AVI Controller username nsxtAlbControllerUser: admin # AVI Controller password nsxtAlbControllerPassword: CHANGE-ME - # AVI Controller importable Cloud name + # AVI Controller importable Cloud name nsxtAlbImportableCloud: NSXT AVI Cloud # Service Engine Group name within (Should be configured in Active Standby mode) nsxtAlbServiceEngineGroup: active-standby-service-engine-group + # IP Discovery profile defined in NSX-T Manager + ipDiscoveryProfile: "ip-discovery-profile" + # MAC Discovery profile defined in NSX-T Manager + macDiscoveryProfile: "mac-discovery-profile" + # Spoof Guard profile defined in NSX-T Manager + spoofGuardProfile: "spoof-guard-profile" + # QoS profile defined in NSX-T Manager + qosProfile: "qos-profile" + # Segment Security profile defined in NSX-T Manager + segmentSecurityProfile: "segment-security-profile" # An Org catalog, possibly containing at least one item catalog: name: mycat @@ -119,6 +131,8 @@ vcd: # One item in the NSX-T catalog. It will be used to compose test vApps. Some tests rely on it # being Photon OS. If it is not Photon OS - some tests will be skipped nsxtCatalogItem: my-nsxt-item + # Item in the NSX-T catalog that has a newer hardware version and supports EFI boot. + catalogItemWithEfiSupport: my-cat-item-with-efi-support # # An optional description for the catalog. Its test will be skipped if omitted. # If provided, it must be the current description of the catalog @@ -237,3 +251,24 @@ media: mediaName: uploadedMediaName # Existing media in NSX-T backed VDC nsxtBackedMediaName: nsxtMediaName + # A valid UI Plugin to use in tests + uiPluginPath: ../test-resources/ui_plugin.zip +cse: + # The CSE version installed in VCD + version: "4.2.0" + # The organization where Container Service Extension (CSE) Server is running + solutionsOrg: "solutions_org" + # The organization where the Kubernetes clusters are created + tenantOrg: "tenant_org" + # The VDC where the Kubernetes clusters are created + tenantVdc: "tenant_vdc" + # The network which the Kubernetes clusters use + routedNetwork: "tenant_net_routed" + # The edge gateway which the Kubernetes clusters use + edgeGateway: "tenant_edgegateway" + # The storage profile which the Kubernetes clusters use + storageProfile: "*" + # The catalog which the Kubernetes clusters use + ovaCatalog: "tkgm_catalog" + # The TKGm OVA which the Kubernetes clusters use + ovaName: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go index 7a71e609b..532d64d44 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go @@ -17,6 +17,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/util" ) +const labelGlobalDefaultSegmentProfileTemplate = "Global Default Segment Profile Template" + // Simple structure to pass Edge Gateway creation parameters. type EdgeGatewayCreation struct { ExternalNetworks []string // List of external networks to be linked to this gateway @@ -428,8 +430,9 @@ func getOrgHREFById(vcdClient *VCDClient, orgId string) (string, error) { // E.g. filter could look like: name==vC1 func QueryVirtualCenters(vcdClient *VCDClient, filter string) ([]*types.QueryResultVirtualCenterRecordType, error) { results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "virtualCenter", - "filter": filter, + "type": "virtualCenter", + "filter": filter, + "filterEncode": "true", }) if err != nil { return nil, err @@ -720,6 +723,26 @@ func getExtension(client *Client) (*types.Extension, error) { return extensions, err } +// GetStorageProfileById fetches a storage profile using its ID. +func (vcdClient *VCDClient) GetStorageProfileById(id string) (*types.VdcStorageProfile, error) { + return getStorageProfileById(&vcdClient.Client, id) +} + +// getStorageProfileById fetches a storage profile using its ID. +func getStorageProfileById(client *Client, id string) (*types.VdcStorageProfile, error) { + storageProfileHref := client.VCDHREF + storageProfileHref.Path += "/admin/vdcStorageProfile/" + extractUuid(id) + + vdcStorageProfile := &types.VdcStorageProfile{} + + _, err := client.ExecuteRequest(storageProfileHref.String(), http.MethodGet, "", "error retrieving storage profile: %s", nil, vdcStorageProfile) + if err != nil { + return nil, err + } + + return vdcStorageProfile, nil +} + // GetStorageProfileByHref fetches storage profile using provided HREF. // Deprecated: use client.GetStorageProfileByHref or vcdClient.GetStorageProfileByHref func GetStorageProfileByHref(vcdClient *VCDClient, url string) (*types.VdcStorageProfile, error) { @@ -736,9 +759,7 @@ func (client *Client) GetStorageProfileByHref(url string) (*types.VdcStorageProf vdcStorageProfile := &types.VdcStorageProfile{} - // only from 35.0 API version IOPS settings are added to response - _, err := client.ExecuteRequestWithApiVersion(url, http.MethodGet, - "", "error retrieving storage profile: %s", nil, vdcStorageProfile, client.GetSpecificApiVersionOnCondition(">= 35.0", "35.0")) + _, err := client.ExecuteRequest(url, http.MethodGet, "", "error retrieving storage profile: %s", nil, vdcStorageProfile) if err != nil { return nil, err } @@ -754,9 +775,8 @@ func (client *Client) GetStorageProfileByHref(url string) (*types.VdcStorageProf // 4. [NOT FOUND] The name does not match any of the storage profiles func (vcdClient *VCDClient) QueryProviderVdcStorageProfileByName(name, providerVDCHref string) (*types.QueryResultProviderVdcStorageProfileRecordType, error) { - results, err := vcdClient.Client.cumulativeQuery(types.QtProviderVdcStorageProfile, map[string]string{ - "type": types.QtProviderVdcStorageProfile, - }, nil) + results, err := vcdClient.Client.cumulativeQuery(types.QtProviderVdcStorageProfile, nil, map[string]string{ + "type": types.QtProviderVdcStorageProfile}) if err != nil { return nil, err } @@ -940,6 +960,18 @@ func (vcdClient *VCDClient) QueryNsxtManagerByName(name string) ([]*types.QueryR return results.Results.NsxtManagerRecord, nil } +// QueryNsxtManagers retrieves all NSX-T managers available in VCD +func (vcdClient *VCDClient) QueryNsxtManagers() ([]*types.QueryResultNsxtManagerRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "nsxTManager", + }) + if err != nil { + return nil, err + } + + return results.Results.NsxtManagerRecord, nil +} + // QueryNsxtManagerByHref searches for NSX-T managers available in VCD func (vcdClient *VCDClient) QueryNsxtManagerByHref(href string) ([]*types.QueryResultNsxtManagerRecordType, error) { results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ @@ -1141,6 +1173,38 @@ func QueryAdminOrgVdcStorageProfileByID(vcdCli *VCDClient, id string) (*types.Qu return results.Results.AdminOrgVdcStorageProfileRecord[0], nil } +// queryAdminOrgVdcStorageProfilesByVdcId finds all Storage Profiles of a VDC +func queryAdminOrgVdcStorageProfilesByVdcId(client *Client, vdcId string) ([]*types.QueryResultAdminOrgVdcStorageProfileRecordType, error) { + if !client.IsSysAdmin { + return nil, errors.New("can't query type QueryResultAdminOrgVdcStorageProfileRecordType as Tenant user") + } + results, err := client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtAdminOrgVdcStorageProfile, + "filter": fmt.Sprintf("vdc==%s", url.QueryEscape(vdcId)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + return results.Results.AdminOrgVdcStorageProfileRecord, nil +} + +// queryOrgVdcStorageProfilesByVdcId finds all Storage Profiles of a VDC +func queryOrgVdcStorageProfilesByVdcId(client *Client, vdcId string) ([]*types.QueryResultOrgVdcStorageProfileRecordType, error) { + if client.IsSysAdmin { + return nil, errors.New("can't query type QueryResultAdminOrgVdcStorageProfileRecordType as System administrator") + } + results, err := client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtOrgVdcStorageProfile, + "filter": fmt.Sprintf("vdc==%s", url.QueryEscape(vdcId)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + return results.Results.OrgVdcStorageProfileRecord, nil +} + // QueryOrgVdcStorageProfileByID finds a StorageProfile of VDC by ID func QueryOrgVdcStorageProfileByID(vcdCli *VCDClient, id string) (*types.QueryResultOrgVdcStorageProfileRecordType, error) { if vcdCli.Client.IsSysAdmin { @@ -1162,3 +1226,22 @@ func QueryOrgVdcStorageProfileByID(vcdCli *VCDClient, id string) (*types.QueryRe } return results.Results.OrgVdcStorageProfileRecord[0], nil } + +// GetGlobalDefaultSegmentProfileTemplates retrieves VCD global configuration for Segment Profile Templates +func (vcdClient *VCDClient) GetGlobalDefaultSegmentProfileTemplates() (*types.NsxtGlobalDefaultSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates, + entityLabel: labelGlobalDefaultSegmentProfileTemplate, + } + + return getInnerEntity[types.NsxtGlobalDefaultSegmentProfileTemplate](&vcdClient.Client, c) +} + +// UpdateGlobalDefaultSegmentProfileTemplates updates VCD global configuration for Segment Profile Templates +func (vcdClient *VCDClient) UpdateGlobalDefaultSegmentProfileTemplates(entityConfig *types.NsxtGlobalDefaultSegmentProfileTemplate) (*types.NsxtGlobalDefaultSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates, + entityLabel: labelGlobalDefaultSegmentProfileTemplate, + } + return updateInnerEntity(&vcdClient.Client, c, entityConfig) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go index f224e96b8..9e59d19de 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go @@ -60,7 +60,7 @@ type DhcpSettings struct { } // Returns the vdc where the vapp resides in. -func (vapp *VApp) getParentVDC() (Vdc, error) { +func (vapp *VApp) GetParentVDC() (Vdc, error) { for _, link := range vapp.VApp.Link { if (link.Type == types.MimeVDC || link.Type == types.MimeAdminVDC) && link.Rel == "up" { @@ -159,9 +159,11 @@ func (vapp *VApp) AddRawVM(vAppComposition *types.ReComposeVAppParams) (*VM, err apiEndpoint.Path += "/action/recomposeVApp" // Return the task - task, err := vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, types.MimeRecomposeVappParams, "error instantiating a new VM: %s", vAppComposition) + task, err := vapp.client.ExecuteTaskRequestWithApiVersion(apiEndpoint.String(), http.MethodPost, + types.MimeRecomposeVappParams, "error instantiating a new VM: %s", + vAppComposition, vapp.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) if err != nil { - return nil, fmt.Errorf("error instantiating a new VM: %s", err) + return nil, err } err = task.WaitTaskCompletion() @@ -618,7 +620,7 @@ func (vapp *VApp) ChangeStorageProfile(name string) (Task, error) { return Task{}, fmt.Errorf("vApp doesn't contain any children, interrupting customization") } - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { return Task{}, fmt.Errorf("error retrieving parent VDC for vApp %s", vapp.VApp.Name) } @@ -1440,7 +1442,7 @@ func (vapp *VApp) getOrgInfo() (*TenantContext, error) { return previous, nil } var err error - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { return nil, err } @@ -1512,7 +1514,7 @@ func (vapp *VApp) Rename(newName string) error { } func (vapp *VApp) getTenantContext() (*TenantContext, error) { - parentVdc, err := vapp.getParentVDC() + parentVdc, err := vapp.GetParentVDC() if err != nil { return nil, err } @@ -1540,7 +1542,7 @@ func (vapp *VApp) RenewLease(deploymentLeaseInSeconds, storageLeaseInSeconds int } } if href == "" { - return fmt.Errorf("link to update lease sttings not found for vApp %s", vapp.VApp.Name) + return fmt.Errorf("link to update lease settings not found for vApp %s", vapp.VApp.Name) } var leaseSettings = types.UpdateLeaseSettingsSection{ diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp_network.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp_network.go index 5f3acb6cc..bd95458e0 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp_network.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp_network.go @@ -9,6 +9,7 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" "net/http" + "strings" ) // UpdateNetworkFirewallRules updates vApp networks firewall rules. It will overwrite existing ones as there is @@ -300,3 +301,78 @@ func (vapp *VApp) RemoveAllNetworkStaticRoutes(networkId string) error { } return nil } + +// queryVappNetworks returns a list of vApp networks with an optional filter +func queryVappNetworks(client *Client, values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + + vAppNetworkType := types.QtVappNetwork + if client.IsSysAdmin { + vAppNetworkType = types.QtAdminVappNetwork + } + + params := map[string]string{ + "type": vAppNetworkType, + } + filterValue := "" + if len(values) > 0 { + var filterElements []string + for k, v := range values { + item := fmt.Sprintf("%s==%s", k, v) + filterElements = append(filterElements, item) + } + filterValue = strings.Join(filterElements, ";") + } + if filterValue != "" { + params["filter"] = filterValue + } + results, err := client.cumulativeQuery(vAppNetworkType, nil, params) + if err != nil { + return nil, fmt.Errorf("error retrieving vApp networks %s", err) + } + + if client.IsSysAdmin { + return results.Results.AdminVappNetworkRecord, nil + } + return results.Results.VappNetworkRecord, nil +} + +// QueryVappNetworks returns all vApp networks visible to the client +func (client *Client) QueryVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + return queryVappNetworks(client, values) +} + +// QueryAllVappNetworks returns all vApp networks and vApp Org Networks belonging to the current vApp +func (vapp *VApp) QueryAllVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{"vApp": extractUuid(vapp.VApp.ID)} + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} + +// QueryVappNetworks returns all vApp networks belonging to the current vApp +func (vapp *VApp) QueryVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{ + "vApp": extractUuid(vapp.VApp.ID), + "isLinked": "false", + } + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} + +// QueryVappOrgNetworks returns all vApp networks belonging to the current vApp +func (vapp *VApp) QueryVappOrgNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{ + "vApp": extractUuid(vapp.VApp.ID), + "isLinked": "true", + } + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go index 21e3c12d2..2044cde24 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapptemplate.go @@ -6,10 +6,11 @@ package govcd import ( "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" - "github.com/vmware/go-vcloud-director/v2/util" "net/http" "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) type VAppTemplate struct { @@ -293,3 +294,93 @@ func (vcdClient *VCDClient) QuerySynchronizedVmInVAppTemplateByHref(vAppTemplate } return vmRecord, nil } + +// RenewLease updates the lease terms for the vAppTemplate +func (vAppTemplate *VAppTemplate) RenewLease(storageLeaseInSeconds int) error { + + href := "" + if vAppTemplate.VAppTemplate.LeaseSettingsSection != nil { + if vAppTemplate.VAppTemplate.LeaseSettingsSection.StorageLeaseInSeconds == storageLeaseInSeconds { + // Requested parameters are the same as existing parameters: exit without updating + return nil + } + href = vAppTemplate.VAppTemplate.LeaseSettingsSection.HREF + } + if href == "" { + href = getUrlFromLink(vAppTemplate.VAppTemplate.Link, "edit", types.MimeLeaseSettingSection) + } + + if href == "" { + return fmt.Errorf("link to update lease settings not found for vAppTemplate %s", vAppTemplate.VAppTemplate.Name) + } + + var leaseSettings = types.UpdateLeaseSettingsSection{ + HREF: href, + XmlnsOvf: types.XMLNamespaceOVF, + Xmlns: types.XMLNamespaceVCloud, + OVFInfo: "Lease section settings", + Type: types.MimeLeaseSettingSection, + StorageLeaseInSeconds: &storageLeaseInSeconds, + } + + task, err := vAppTemplate.client.ExecuteTaskRequest(href, http.MethodPut, + types.MimeLeaseSettingSection, "error updating vAppTemplate lease : %s", &leaseSettings) + + if err != nil { + return fmt.Errorf("unable to update vAppTemplate lease: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("task for updating vAppTemplate lease failed: %s", err) + } + return vAppTemplate.Refresh() +} + +// GetLease retrieves the lease terms for a vAppTemplate +func (vAppTemplate *VAppTemplate) GetLease() (*types.LeaseSettingsSection, error) { + + href := "" + if vAppTemplate.VAppTemplate.LeaseSettingsSection != nil { + href = vAppTemplate.VAppTemplate.LeaseSettingsSection.HREF + } + if href == "" { + for _, link := range vAppTemplate.VAppTemplate.Link { + if link.Type == types.MimeLeaseSettingSection { + href = link.HREF + break + } + } + } + if href == "" { + return nil, fmt.Errorf("link to retrieve lease settings not found for vApp %s", vAppTemplate.VAppTemplate.Name) + } + var leaseSettings types.LeaseSettingsSection + + _, err := vAppTemplate.client.ExecuteRequest(href, http.MethodGet, "", "error getting vAppTemplate lease info: %s", nil, &leaseSettings) + + if err != nil { + return nil, err + } + return &leaseSettings, nil +} + +// GetCatalogItemHref looks up Href for catalog item in vApp template +func (vAppTemplate *VAppTemplate) GetCatalogItemHref() (string, error) { + for _, link := range vAppTemplate.VAppTemplate.Link { + if link.Rel == "catalogItem" && link.Type == types.MimeCatalogItem { + return link.HREF, nil + } + } + return "", fmt.Errorf("error finding Catalog Item link in vApp template %s", vAppTemplate.VAppTemplate.ID) +} + +// GetCatalogItemId returns ID for catalog item in vApp template +func (vAppTemplate *VAppTemplate) GetCatalogItemId() (string, error) { + href, err := vAppTemplate.GetCatalogItemHref() + if err != nil { + return "", err + } + + return fmt.Sprintf("urn:vcloud:catalogitem:%s", extractUuid(href)), nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go index 1740e4bba..05cffacb9 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc.go @@ -389,6 +389,14 @@ func (vdc *Vdc) GetEdgeGatewayByHref(href string) (*EdgeGateway, error) { if err != nil { return nil, err } + + // Edge gateways can sometimes come without any configured services which + // lead to nil pointer dereference when adding e.g a DNAT rule + // https://github.com/vmware/go-vcloud-director/issues/585 + if edge.EdgeGateway.Configuration.EdgeGatewayServiceConfiguration == nil { + edge.EdgeGateway.Configuration.EdgeGatewayServiceConfiguration = &types.GatewayFeatures{} + } + return edge, nil } @@ -1030,7 +1038,10 @@ func (vdc *Vdc) CreateStandaloneVmAsync(params *types.CreateVmParams) (Task, err } params.XmlnsOvf = types.XMLNamespaceOVF - return vdc.client.ExecuteTaskRequest(href, http.MethodPost, types.MimeCreateVmParams, "error creating standalone VM: %s", params) + // 37.1 Introduced new parameters to VM configuration + return vdc.client.ExecuteTaskRequestWithApiVersion(href, http.MethodPost, + types.MimeCreateVmParams, "error creating standalone VM: %s", params, + vdc.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) } // getVmFromTask finds a VM from a running standalone VM creation task @@ -1342,3 +1353,70 @@ func (vdc *Vdc) CloneVapp(sourceVapp *types.CloneVAppParams) (*VApp, error) { err = vapp.Refresh() return vapp, err } + +// Get the details of a hardware version +func (vdc *Vdc) GetHardwareVersion(name string) (*types.VirtualHardwareVersion, error) { + if len(vdc.Vdc.Capabilities) == 0 { + return nil, fmt.Errorf("VDC doesn't have any virtual hardware version support information stored") + } + + found := false + for _, hwVersion := range vdc.Vdc.Capabilities[0].SupportedHardwareVersions.SupportedHardwareVersion { + if hwVersion.Name == name { + found = true + } + } + if !found { + return nil, fmt.Errorf("hardware version %s not found or not supported", name) + } + + vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) + if err != nil { + return nil, fmt.Errorf("error getting VDC href: %s", err) + } + vdcHref.Path += "/hwv/" + name + + hardwareVersion := &types.VirtualHardwareVersion{} + + _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodGet, types.MimeVirtualHardwareVersion, "error getting hardware version: %s", nil, hardwareVersion) + if err != nil { + return nil, err + } + + return hardwareVersion, nil +} + +// Get highest supported hardware version of a VDC +func (vdc *Vdc) GetHighestHardwareVersion() (*types.VirtualHardwareVersion, error) { + err := vdc.Refresh() + if err != nil { + return nil, err + } + + if len(vdc.Vdc.Capabilities) == 0 { + return nil, fmt.Errorf("VDC doesn't have any virtual hardware version support information stored") + } + + hardwareVersions := vdc.Vdc.Capabilities[0].SupportedHardwareVersions.SupportedHardwareVersion + // Get last item (highest version) of SupportedHardwareVersions + highestVersion := hardwareVersions[len(hardwareVersions)-1].Name + + hardwareVersion, err := vdc.GetHardwareVersion(highestVersion) + if err != nil { + return nil, err + } + return hardwareVersion, nil +} + +// FindOsFromId attempts to find a OS by ID using the given hardware version +func (vdc *Vdc) FindOsFromId(hardwareVersion *types.VirtualHardwareVersion, osId string) (*types.OperatingSystemInfoType, error) { + for _, osFamily := range hardwareVersion.SupportedOperatingSystems.OperatingSystemFamilyInfo { + for _, os := range osFamily.OperatingSystems { + if osId == os.InternalName { + return os, nil + } + } + } + + return nil, fmt.Errorf("no OS found with ID: %s", osId) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_group.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_group.go index df7e40a31..a071f6c0a 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_group.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_group.go @@ -168,6 +168,11 @@ func (adminOrg *AdminOrg) GetAllVdcGroupCandidates(queryParameters url.Values) ( // Delete deletes VDC group func (vdcGroup *VdcGroup) Delete() error { + return vdcGroup.ForceDelete(false) +} + +// ForceDelete deletes VDC group with force parameter if enabled +func (vdcGroup *VdcGroup) ForceDelete(force bool) error { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { @@ -183,10 +188,14 @@ func (vdcGroup *VdcGroup) Delete() error { return err } - err = vdcGroup.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + params := copyOrNewUrlValues(nil) + if force { + params.Add("force", "true") + } + err = vdcGroup.client.OpenApiDeleteItem(minimumApiVersion, urlRef, params, nil) if err != nil { - return fmt.Errorf("error deleting VDC group: %s", err) + return fmt.Errorf("error deleting VDC group (force %t): %s", force, err) } return nil diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_network_profile.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_network_profile.go index 773e55be4..150639e55 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_network_profile.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdc_network_profile.go @@ -10,6 +10,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelVdcNetworkProfile = "VDC Network Profile" + // VDC Network profiles have 1:1 mapping with VDC - each VDC has an option to configure VDC Network // Profiles. types.VdcNetworkProfile holds more information about possible configurations @@ -75,72 +77,28 @@ func (adminVdc *AdminVdc) DeleteVdcNetworkProfile() error { } func getVdcNetworkProfile(client *Client, vdcId string) (*types.VdcNetworkProfile, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - if vdcId == "" { - return nil, fmt.Errorf("cannot lookup VDC Network Profile configuration without VDC ID") - } - - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcId)) - if err != nil { - return nil, err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, } - - returnObject := &types.VdcNetworkProfile{} - err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject, nil) - if err != nil { - return nil, err - } - - return returnObject, nil + return getInnerEntity[types.VdcNetworkProfile](client, c) } func updateVdcNetworkProfile(client *Client, vdcId string, vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return nil, err - } - - if vdcId == "" { - return nil, fmt.Errorf("cannot update VDC Network Profile configuration without ID") + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, } - - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcId)) - if err != nil { - return nil, err - } - - returnObject := &types.VdcNetworkProfile{} - err = client.OpenApiPutItem(apiVersion, urlRef, nil, vdcNetworkProfileConfig, returnObject, nil) - if err != nil { - return nil, fmt.Errorf("error updating VDC Network Profile configuration: %s", err) - } - - return returnObject, nil + return updateInnerEntity(client, c, vdcNetworkProfileConfig) } func deleteVdcNetworkProfile(client *Client, vdcId string) error { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile - apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) - if err != nil { - return err + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, } - - urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcId)) - if err != nil { - return err - } - - err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - - if err != nil { - return fmt.Errorf("error deleting VDC Network Profile configuration: %s", err) - } - - return nil + return deleteEntityById(client, c) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdccomputepolicy_v2.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdccomputepolicy_v2.go index fc1dc5055..68cc47a9b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdccomputepolicy_v2.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vdccomputepolicy_v2.go @@ -23,13 +23,13 @@ type VdcComputePolicyV2 struct { // GetVdcComputePolicyV2ById retrieves VDC Compute Policy (V2) by given ID func (client *VCDClient) GetVdcComputePolicyV2ById(id string) (*VdcComputePolicyV2, error) { - return getVdcComputePolicyV2ById(client, id) + return getVdcComputePolicyV2ById(&client.Client, id) } // getVdcComputePolicyV2ById retrieves VDC Compute Policy (V2) by given ID -func getVdcComputePolicyV2ById(client *VCDClient, id string) (*VdcComputePolicyV2, error) { +func getVdcComputePolicyV2ById(client *Client, id string) (*VdcComputePolicyV2, error) { endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies - minimumApiVersion, err := client.Client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { return nil, err } @@ -38,7 +38,7 @@ func getVdcComputePolicyV2ById(client *VCDClient, id string) (*VdcComputePolicyV return nil, fmt.Errorf("empty VDC id") } - urlRef, err := client.Client.OpenApiBuildEndpoint(endpoint, id) + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) if err != nil { return nil, err @@ -47,10 +47,10 @@ func getVdcComputePolicyV2ById(client *VCDClient, id string) (*VdcComputePolicyV vdcComputePolicy := &VdcComputePolicyV2{ VdcComputePolicyV2: &types.VdcComputePolicyV2{}, Href: urlRef.String(), - client: &client.Client, + client: client, } - err = client.Client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicyV2, nil) + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicyV2, nil) if err != nil { return nil, err } @@ -61,26 +61,26 @@ func getVdcComputePolicyV2ById(client *VCDClient, id string) (*VdcComputePolicyV // GetAllVdcComputePoliciesV2 retrieves all VDC Compute Policies (V2) using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering func (client *VCDClient) GetAllVdcComputePoliciesV2(queryParameters url.Values) ([]*VdcComputePolicyV2, error) { - return getAllVdcComputePoliciesV2(client, queryParameters) + return getAllVdcComputePoliciesV2(&client.Client, queryParameters) } // getAllVdcComputePolicies retrieves all VDC Compute Policies (V2) using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering -func getAllVdcComputePoliciesV2(client *VCDClient, queryParameters url.Values) ([]*VdcComputePolicyV2, error) { +func getAllVdcComputePoliciesV2(client *Client, queryParameters url.Values) ([]*VdcComputePolicyV2, error) { endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies - minimumApiVersion, err := client.Client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { return nil, err } - urlRef, err := client.Client.OpenApiBuildEndpoint(endpoint) + urlRef, err := client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } responses := []*types.VdcComputePolicyV2{{}} - err = client.Client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) if err != nil { return nil, err } @@ -88,7 +88,7 @@ func getAllVdcComputePoliciesV2(client *VCDClient, queryParameters url.Values) ( var wrappedVdcComputePolicies []*VdcComputePolicyV2 for _, response := range responses { wrappedVdcComputePolicy := &VdcComputePolicyV2{ - client: &client.Client, + client: client, VdcComputePolicyV2: response, } wrappedVdcComputePolicies = append(wrappedVdcComputePolicies, wrappedVdcComputePolicy) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vgpu_profile.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vgpu_profile.go new file mode 100644 index 000000000..b42e23d0f --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vgpu_profile.go @@ -0,0 +1,155 @@ +package govcd + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// VgpuProfile defines a vGPU profile which is fetched from vCenter +type VgpuProfile struct { + VgpuProfile *types.VgpuProfile + client *Client +} + +// GetAllVgpuProfiles gets all vGPU profiles that are available to VCD +func (client *VCDClient) GetAllVgpuProfiles(queryParameters url.Values) ([]*VgpuProfile, error) { + return getAllVgpuProfiles(queryParameters, &client.Client) +} + +// GetVgpuProfilesByProviderVdc gets all vGPU profiles that are available to a specific provider VDC +func (client *VCDClient) GetVgpuProfilesByProviderVdc(providerVdcUrn string) ([]*VgpuProfile, error) { + queryParameters := url.Values{} + queryParameters = queryParameterFilterAnd(fmt.Sprintf("pvdcId==%s", providerVdcUrn), queryParameters) + return client.GetAllVgpuProfiles(queryParameters) +} + +// GetVgpuProfileById gets a vGPU profile by ID +func (client *VCDClient) GetVgpuProfileById(vgpuProfileId string) (*VgpuProfile, error) { + return getVgpuProfileById(vgpuProfileId, &client.Client) +} + +// GetVgpuProfileByName gets a vGPU profile by name +func (client *VCDClient) GetVgpuProfileByName(vgpuProfileName string) (*VgpuProfile, error) { + return getVgpuProfileByFilter("name", vgpuProfileName, &client.Client) +} + +// GetVgpuProfileByTenantFacingName gets a vGPU profile by its tenant facing name +func (client *VCDClient) GetVgpuProfileByTenantFacingName(tenantFacingName string) (*VgpuProfile, error) { + return getVgpuProfileByFilter("tenantFacingName", tenantFacingName, &client.Client) +} + +// Update updates a vGPU profile with new parameters +func (profile *VgpuProfile) Update(newProfile *types.VgpuProfile) error { + client := profile.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, "/", profile.VgpuProfile.Id) + if err != nil { + return err + } + + err = client.OpenApiPutItemSync(minimumApiVersion, urlRef, nil, newProfile, nil, nil) + if err != nil { + return err + } + + // We need to refresh here, as PUT returns the original struct instead of the updated one + err = profile.Refresh() + if err != nil { + return err + } + + return nil +} + +// Refresh updates the current state of the vGPU profile +func (profile *VgpuProfile) Refresh() error { + var err error + newProfile, err := getVgpuProfileById(profile.VgpuProfile.Id, profile.client) + if err != nil { + return err + } + profile.VgpuProfile = newProfile.VgpuProfile + + return nil +} + +func getVgpuProfileByFilter(filter, filterValue string, client *Client) (*VgpuProfile, error) { + queryParameters := url.Values{} + queryParameters = queryParameterFilterAnd(fmt.Sprintf("%s==%s", filter, filterValue), queryParameters) + vgpuProfiles, err := getAllVgpuProfiles(queryParameters, client) + if err != nil { + return nil, err + } + + vgpuProfile, err := oneOrError(filter, filterValue, vgpuProfiles) + if err != nil { + return nil, err + } + + return vgpuProfile, nil +} + +func getVgpuProfileById(vgpuProfileId string, client *Client) (*VgpuProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, "/", vgpuProfileId) + if err != nil { + return nil, err + } + + profile := &VgpuProfile{ + client: client, + } + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, &profile.VgpuProfile, nil) + if err != nil { + return nil, err + } + + return profile, nil +} + +func getAllVgpuProfiles(queryParameters url.Values, client *Client) ([]*VgpuProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.VgpuProfile{{}} + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) + if err != nil { + return nil, err + } + + wrappedVgpuProfiles := make([]*VgpuProfile, len(responses)) + for index, response := range responses { + wrappedVgpuProfile := &VgpuProfile{ + client: client, + VgpuProfile: response, + } + wrappedVgpuProfiles[index] = wrappedVgpuProfile + } + + return wrappedVgpuProfiles, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index 6dd29885a..055457b1d 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -70,7 +70,8 @@ func (vm *VM) Refresh() error { // elements in slices. vm.VM = &types.Vm{} - _, err := vm.client.ExecuteRequest(refreshUrl, http.MethodGet, "", "error refreshing VM: %s", nil, vm.VM) + // 37.1 Introduced BootOptions and Firmware parameters of a VM + _, err := vm.client.ExecuteRequestWithApiVersion(refreshUrl, http.MethodGet, "", "error refreshing VM: %s", nil, vm.VM, vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) // The request was successful return err @@ -924,7 +925,7 @@ func (vm *VM) GetParentVdc() (*Vdc, error) { return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err) } - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err) } @@ -1261,8 +1262,8 @@ func (vm *VM) validateInternalDiskInput(diskData *types.DiskSettings, vmName, vm return fmt.Errorf("[VM %s Id %s] disk settings size MB has to be 0 or higher", vmName, vmId) } - if diskData.Iops != nil && *diskData.Iops < int64(0) { - return fmt.Errorf("[VM %s Id %s] disk settings iops has to be 0 or higher", vmName, vmId) + if diskData.IopsAllocation != nil && diskData.IopsAllocation.Reservation < int64(0) { + return fmt.Errorf("[VM %s Id %s] disk settings iops reservation has to be 0 or higher", vmName, vmId) } if diskData.ThinProvisioned == nil { @@ -1352,26 +1353,22 @@ func (vm *VM) UpdateInternalDisks(disksSettingToUpdate *types.VmSpecSection) (*t return nil, fmt.Errorf("cannot update internal disks - VM HREF is unset") } - task, err := vm.UpdateInternalDisksAsync(disksSettingToUpdate) + description := vm.VM.Description + vm, err := vm.UpdateVmSpecSection(disksSettingToUpdate, description) if err != nil { return nil, err } - err = task.WaitTaskCompletion() - if err != nil { - return nil, fmt.Errorf("error waiting for task completion after internal disks update for VM %s: %s", vm.VM.Name, err) - } - err = vm.Refresh() - if err != nil { - return nil, fmt.Errorf("error refreshing VM %s: %s", vm.VM.Name, err) - } + return vm.VM.VmSpecSection, nil } // UpdateInternalDisksAsync applies disks configuration for the VM. -// types.VmSpecSection has to have all internal disk state. Disks which don't match provided ones in types.VmSpecSection -// will be deleted. Matched internal disk will be updated. New internal disk description found -// in types.VmSpecSection will be created. +// types.VmSpecSection has to have all internal disk state. Disks which don't +// match provided ones in types.VmSpecSection will be deleted. +// Matched internal disk will be updated. New internal disk description found in types.VmSpecSection will be created. // Returns Task and error. +// +// Deprecated: use UpdateInternalDisks or UpdateVmSpecSectionAsync instead func (vm *VM) UpdateInternalDisksAsync(disksSettingToUpdate *types.VmSpecSection) (Task, error) { if vm.VM.HREF == "" { return Task{}, fmt.Errorf("cannot update disks, VM HREF is unset") @@ -1481,6 +1478,11 @@ func (vm *VM) UpdateVmSpecSectionAsync(vmSettingsToUpdate *types.VmSpecSection, return Task{}, fmt.Errorf("cannot update VM spec section, VM HREF is unset") } + // Firmware field is unavailable on <37.1 API Versions + if vmSettingsToUpdate.Firmware != "" && vm.client.APIVCDMaxVersionIs("<37.1") { + return Task{}, fmt.Errorf("VM Firmware can only be set on VCD 10.4.1+ (API 37.1+)") + } + vmSpecSectionModified := true vmSettingsToUpdate.Modified = &vmSpecSectionModified @@ -1491,14 +1493,18 @@ func (vm *VM) UpdateVmSpecSectionAsync(vmSettingsToUpdate *types.VmSpecSection, // GuestCustomizationSection // Sections not included in the request body will not be updated. - return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, - types.MimeVM, "error updating VM spec section: %s", &types.Vm{ - Xmlns: types.XMLNamespaceVCloud, - Ovf: types.XMLNamespaceOVF, - Name: vm.VM.Name, - Description: description, - VmSpecSection: vmSettingsToUpdate, - }) + vmPayload := &types.Vm{ + Xmlns: types.XMLNamespaceVCloud, + Ovf: types.XMLNamespaceOVF, + Name: vm.VM.Name, + Description: description, + VmSpecSection: vmSettingsToUpdate, + } + + // Since 37.1 there is a Firmware field in VmSpecSection + return vm.client.ExecuteTaskRequestWithApiVersion(vm.VM.HREF+"/action/reconfigureVm", + http.MethodPost, types.MimeVM, "error updating VM spec section: %s", vmPayload, + vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) } // UpdateComputePolicyV2 updates VM Compute policy with the given compute policies using v2.0.0 OpenAPI endpoint, @@ -1656,7 +1662,10 @@ func (client *Client) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryRes // QueryVmList returns a list of all VMs in a given Org func (org *Org) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryResultVMRecordType, error) { - return queryVmList(filter, org.client, "org", org.Org.HREF) + if org.client.IsSysAdmin { + return queryVmList(filter, org.client, "org", org.Org.HREF) + } + return queryVmList(filter, org.client, "", "") } // QueryVmList returns a list of all VMs in a given VDC @@ -1676,11 +1685,42 @@ func queryVmList(filter types.VmQueryFilter, client *Client, filterParent, filte if filter.String() != "" { filterText = filter.String() } - if filterText == "" { - filterText = fmt.Sprintf("%s==%s", filterParent, filterParentHref) - } else { - filterText = fmt.Sprintf("%s;%s==%s", filterText, filterParent, filterParentHref) + if filterParent != "" { + if filterText == "" { + filterText = fmt.Sprintf("%s==%s", filterParent, filterParentHref) + } else { + filterText = fmt.Sprintf("%s;%s==%s", filterText, filterParent, filterParentHref) + } + params["filter"] = filterText + } + vmResult, err := client.cumulativeQuery(queryType, nil, params) + if err != nil { + return nil, fmt.Errorf("error getting VM list : %s", err) + } + vmList = vmResult.Results.VMRecord + if client.IsSysAdmin { + vmList = vmResult.Results.AdminVMRecord + } + return vmList, nil +} + +// QueryVmList retrieves a list of VMs across all VDC, using parameters defined in searchParams +func QueryVmList(vmType types.VmQueryFilter, client *Client, searchParams map[string]string) ([]*types.QueryResultVMRecordType, error) { + var vmList []*types.QueryResultVMRecordType + queryType := client.GetQueryType(types.QtVm) + params := map[string]string{ + "type": queryType, + "filterEncoded": "true", + } + filterText := "" + if vmType.String() != "" { + // The first filter will be the type of VM, i.e. deployed (inside a vApp) or not (inside a vApp template) + filterText = vmType.String() + } + for k, v := range searchParams { + filterText = fmt.Sprintf("%s;%s==%s", filterText, k, v) } + params["filter"] = filterText vmResult, err := client.cumulativeQuery(queryType, nil, params) if err != nil { @@ -1778,8 +1818,9 @@ func addEmptyVmAsyncV10(vapp *VApp, reComposeVAppParams *types.RecomposeVAppPara reComposeVAppParams.XmlnsOvf = types.XMLNamespaceOVF // Return the task - return vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, - types.MimeRecomposeVappParams, "error instantiating a new VM: %s", reComposeVAppParams) + return vapp.client.ExecuteTaskRequestWithApiVersion(apiEndpoint.String(), http.MethodPost, + types.MimeRecomposeVappParams, "error instantiating a new VM: %s", reComposeVAppParams, + vapp.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) } // addEmptyVmV10 adds an empty VM (without template) to vApp and returns the new created VM or an error. @@ -1826,8 +1867,8 @@ func getVMByHrefV10(client *Client, vmHref string) (*VM, error) { newVm := NewVM(client) - _, err := client.ExecuteRequest(vmHref, http.MethodGet, - "", "error retrieving vm: %s", nil, newVm.VM) + _, err := client.ExecuteRequestWithApiVersion(vmHref, http.MethodGet, + "", "error retrieving vm: %s", nil, newVm.VM, client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) if err != nil { @@ -1893,7 +1934,7 @@ func (vm *VM) UpdateStorageProfileAsync(storageProfileHref string) (Task, error) // GuestCustomizationSection // Sections not included in the request body will not be updated. return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, - types.MimeVM, "error updating VM spec section: %s", &types.Vm{ + types.MimeVM, "error updating VM storage profile: %s", &types.Vm{ Xmlns: types.XMLNamespaceVCloud, Ovf: types.XMLNamespaceOVF, Name: vm.VM.Name, @@ -1902,6 +1943,58 @@ func (vm *VM) UpdateStorageProfileAsync(storageProfileHref string) (Task, error) }) } +// UpdateBootOptions updates the Boot Options of a VM and returns the updated instance of the VM +func (vm *VM) UpdateBootOptions(bootOptions *types.BootOptions) (*VM, error) { + task, err := vm.UpdateBootOptionsAsync(bootOptions) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + + err = vm.Refresh() + if err != nil { + return nil, err + } + + return vm, nil +} + +// UpdateBootOptionsAsync updates the boot options of a VM +func (vm *VM) UpdateBootOptionsAsync(bootOptions *types.BootOptions) (Task, error) { + if vm.VM.HREF == "" { + return Task{}, fmt.Errorf("cannot update VM boot options, VM HREF is unset") + } + + if vm.client.APIVCDMaxVersionIs("<37.1") { + + if bootOptions.BootRetryEnabled != nil || bootOptions.BootRetryDelay != nil || + bootOptions.EfiSecureBootEnabled != nil || bootOptions.NetworkBootProtocol != "" { + return Task{}, fmt.Errorf("error: Boot retry, EFI Secure Boot and Boot Network Protocol options were introduced in VCD 10.4.1") + } + } + + if bootOptions == nil { + return Task{}, fmt.Errorf("cannot update VM boot options, none given") + } + + return vm.client.ExecuteTaskRequestWithApiVersion(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, + types.MimeVM, "error updating VM boot options: %s", &types.Vm{ + Xmlns: types.XMLNamespaceVCloud, + Ovf: types.XMLNamespaceOVF, + Name: vm.VM.Name, + Description: vm.VM.Description, + // We need to add ComputePolicy in the Request Body or settings will + // be set to default sizing policy set in the VDC if the VM is Not + // compliant with the current sizing policy + ComputePolicy: vm.VM.ComputePolicy, + BootOptions: bootOptions, + }, vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) +} + // DeleteAsync starts a standalone VM deletion, returning a task func (vm *VM) DeleteAsync() (Task, error) { if vm.VM.HREF == "" { @@ -1992,3 +2085,22 @@ func (vm *VM) ChangeCPUAndCoreCount(cpus, cpuCores *int) error { } return nil } + +// ConsolidateDisksAsync triggers VM disk consolidation task +func (vm *VM) ConsolidateDisksAsync() (Task, error) { + if vm.VM.HREF == "" { + return Task{}, fmt.Errorf("cannot consolidate disks, VM HREF is unset") + } + + return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/consolidate", http.MethodPost, + types.AnyXMLMime, "error consolidating VM disks: %s", nil) +} + +// ConsolidateDisks triggers VM disk consolidation task and waits until it is completed +func (vm *VM) ConsolidateDisks() error { + task, err := vm.ConsolidateDisksAsync() + if err != nil { + return err + } + return task.WaitTaskCompletion() +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vsphere_distributed_switch.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vsphere_distributed_switch.go new file mode 100644 index 000000000..85686678d --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vsphere_distributed_switch.go @@ -0,0 +1,39 @@ +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +func (vcdClient *VCDClient) GetAllVcenterDistributedSwitches(vCenterId string, queryParameters url.Values) ([]*types.VcenterDistributedSwitch, error) { + if vCenterId == "" { + return nil, fmt.Errorf("empty vCenter ID") + } + + if !isUrn(vCenterId) { + return nil, fmt.Errorf("vCenter ID is not URN (e.g. 'urn:vcloud:vimserver:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", vCenterId) + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVCenterDistributedSwitch + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("virtualCenter.id=="+vCenterId, queryParams) + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + var typeResponses []*types.VcenterDistributedSwitch + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go index 8e8b8479e..868aae3ec 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go @@ -101,6 +101,8 @@ const ( MimeVM = "application/vnd.vmware.vcloud.vm+xml" // Mime for instantiate vApp template params MimeInstantiateVappTemplateParams = "application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml" + // Mime for capture vApp into template + MimeCaptureVappTemplateParams = "application/vnd.vmware.vcloud.captureVAppParams+xml" // Mime for clone vApp template params MimeCloneVapp = "application/vnd.vmware.vcloud.cloneVAppParams+xml" // Mime for product section @@ -139,6 +141,8 @@ const ( MimeLeaseSettingSection = "application/vnd.vmware.vcloud.leaseSettingsSection+xml" // Mime to publish external catalog PublishExternalCatalog = "application/vnd.vmware.admin.publishExternalCatalogParams+xml" + // Mime to publish a catalog + PublishCatalog = "application/vnd.vmware.admin.publishCatalogParams+xml" // Mime to subscribe to an external catalog MimeSubscribeToExternalCatalog = "application/vnd.vmware.admin.externalCatalogSubscriptionParams+json" // Mime to identify a media item @@ -150,6 +154,8 @@ const ( // Mime to identify organization federation settings (SAML) XML and JSON MimeFederationSettingsXml = "application/vnd.vmware.admin.organizationFederationSettings+xml" MimeFederationSettingsJson = "application/vnd.vmware.admin.organizationFederationSettings+json" + // Mime to handle virtual hardware versions + MimeVirtualHardwareVersion = "application/vnd.vmware.vcloud.virtualHardwareVersion+xml" ) const ( @@ -278,6 +284,8 @@ const ( QtResourcePool = "resourcePool" // Resource Pool QtNetworkPool = "networkPool" // Network Pool QtProviderVdcStorageProfile = "providerVdcStorageProfile" // StorageProfile of Provider VDC + QtVappNetwork = "vAppNetwork" + QtAdminVappNetwork = "adminVAppNetwork" ) // AdminQueryTypes returns the corresponding "admin" query type for each regular type @@ -380,12 +388,17 @@ const ( OpenApiEndpointEdgeGateways = "edgeGateways/" OpenApiEndpointEdgeGatewayQos = "edgeGateways/%s/qos" OpenApiEndpointEdgeGatewayDhcpForwarder = "edgeGateways/%s/dhcpForwarder" + OpenApiEndpointEdgeGatewayDns = "edgeGateways/%s/dns" OpenApiEndpointEdgeGatewaySlaacProfile = "edgeGateways/%s/slaacProfile" OpenApiEndpointEdgeGatewayStaticRoutes = "edgeGateways/%s/routing/staticRoutes/" OpenApiEndpointEdgeGatewayUsedIpAddresses = "edgeGateways/%s/usedIpAddresses" OpenApiEndpointNsxtFirewallRules = "edgeGateways/%s/firewall/rules" + OpenApiEndpointEdgeGatewayL2VpnTunnel = "edgeGateways/%s/l2vpn/tunnels/" + OpenApiEndpointEdgeGatewayL2VpnTunnelStatistics = "edgeGateways/%s/l2vpn/tunnels/%s/metrics" + OpenApiEndpointEdgeGatewayL2VpnTunnelStatus = "edgeGateways/%s/l2vpn/tunnels/%s/status" OpenApiEndpointFirewallGroups = "firewallGroups/" OpenApiEndpointOrgVdcNetworks = "orgVdcNetworks/" + OpenApiEndpointOrgVdcNetworkSegmentProfiles = "orgVdcNetworks/%s/segmentProfiles" OpenApiEndpointOrgVdcNetworksDhcp = "orgVdcNetworks/%s/dhcp" OpenApiEndpointOrgVdcNetworksDhcpBindings = "orgVdcNetworks/%s/dhcp/bindings/" OpenApiEndpointNsxtNatRules = "edgeGateways/%s/nat/rules/" @@ -432,14 +445,25 @@ const ( OpenApiEndpointExtensionsUiTenantsPublish = "extensions/ui/%s/tenants/publish" OpenApiEndpointExtensionsUiTenantsUnpublishAll = "extensions/ui/%s/tenants/unpublishAll" OpenApiEndpointExtensionsUiTenantsUnpublish = "extensions/ui/%s/tenants/unpublish" + OpenApiEndpointImportableTransportZones = "nsxTResources/importableTransportZones" + OpenApiEndpointVCenterDistributedSwitch = "virtualCenters/resources/dvSwitches" + + OpenApiEndpointNsxtSegmentProfileTemplates = "segmentProfileTemplates/" + OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates = "segmentProfileTemplates/default" + OpenApiEndpointNsxtSegmentIpDiscoveryProfiles = "nsxTResources/segmentIpDiscoveryProfiles" + OpenApiEndpointNsxtSegmentMacDiscoveryProfiles = "nsxTResources/segmentMacDiscoveryProfiles" + OpenApiEndpointNsxtSegmentSpoofGuardProfiles = "nsxTResources/segmentSpoofGuardProfiles" + OpenApiEndpointNsxtSegmentQosProfiles = "nsxTResources/segmentQoSProfiles" + OpenApiEndpointNsxtSegmentSecurityProfiles = "nsxTResources/segmentSecurityProfiles" // IP Spaces - OpenApiEndpointIpSpaces = "ipSpaces/" - OpenApiEndpointIpSpaceSummaries = "ipSpaces/summaries" - OpenApiEndpointIpSpaceUplinks = "ipSpaceUplinks/" - OpenApiEndpointIpSpaceUplinksAllocate = "ipSpaces/%s/allocate" // '%s' is IP Space ID - OpenApiEndpointIpSpaceIpAllocations = "ipSpaces/%s/allocations/" // '%s' is IP Space ID - OpenApiEndpointIpSpaceOrgAssignments = "ipSpaces/orgAssignments/" // '%s' is IP Space ID + OpenApiEndpointIpSpaces = "ipSpaces/" + OpenApiEndpointIpSpaceSummaries = "ipSpaces/summaries" + OpenApiEndpointIpSpaceUplinks = "ipSpaceUplinks/" + OpenApiEndpointIpSpaceUplinksAllocate = "ipSpaces/%s/allocate" // '%s' is IP Space ID + OpenApiEndpointIpSpaceIpAllocations = "ipSpaces/%s/allocations/" // '%s' is IP Space ID + OpenApiEndpointIpSpaceOrgAssignments = "ipSpaces/orgAssignments/" // '%s' is IP Space ID + OpenApiEndpointIpSpaceFloatingIpSuggestions = "ipSpaces/floatingIpSuggestions/" // NSX-T ALB related endpoints @@ -463,6 +487,9 @@ const ( OpenApiEndpointServiceAccountGrant = "deviceLookup/grant" OpenApiEndpointTokens = "tokens/" OpenApiEndpointServiceAccounts = "serviceAccounts/" + + // OpenApiEndpointVgpuProfile is used to query vGPU profiles + OpenApiEndpointVgpuProfile = "vgpuProfiles" ) // Header keys to run operations in tenant context @@ -586,6 +613,10 @@ const ( MetadataReadOnlyVisibility string = "READONLY" MetadataHiddenVisibility string = "PRIVATE" MetadataReadWriteVisibility string = "READWRITE" + + OpenApiMetadataStringEntry string = "StringEntry" + OpenApiMetadataNumberEntry string = "NumberEntry" + OpenApiMetadataBooleanEntry string = "BoolEntry" ) const ( @@ -688,3 +719,20 @@ const ( SamlNamespaceDs = "http://www.w3.org/2000/09/xmldsig#" SamlNamespaceHoksso = "urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser" ) + +// Values used to identify the type of network pool +const ( + NetworkPoolVxlanType = "VXLAN" // NSX-V backed network pool. Only used as read-only + NetworkPoolVlanType = "VLAN" + NetworkPoolGeneveType = "GENEVE" + NetworkPoolPortGroupType = "PORTGROUP_BACKED" +) + +// BackingUseConstraint is a constraint about the use of a backing in a network pool +type BackingUseConstraint string + +const ( + BackingUseExplicit BackingUseConstraint = "use-explicit-name" // use explicitly named backing + BackingUseWhenOnlyOne BackingUseConstraint = "use-when-only-one" // use automatically when only one was found + BackingUseFirstAvailable BackingUseConstraint = "use-first-available" // use the first available backing with no conditions +) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/cse.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/cse.go new file mode 100644 index 000000000..31e82b16f --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/cse.go @@ -0,0 +1,225 @@ +package types + +import "time" + +// Capvcd (Cluster API Provider for VCD), is a type that represents a Kubernetes cluster inside VCD, that is created and managed +// with the Container Service Extension (CSE) +type Capvcd struct { + Kind string `json:"kind,omitempty"` + Spec struct { + VcdKe struct { + // NOTE: "Secure" struct needs to be a pointer to avoid overriding with empty values by mistake, as VCD doesn't return RDE fields + // marked with "x-vcloud-restricted: secure" + Secure *struct { + ApiToken string `json:"apiToken,omitempty"` + } `json:"secure,omitempty"` + IsVCDKECluster bool `json:"isVCDKECluster,omitempty"` + AutoRepairOnErrors bool `json:"autoRepairOnErrors,omitempty"` + DefaultStorageClassOptions struct { + Filesystem string `json:"filesystem,omitempty"` + K8SStorageClassName string `json:"k8sStorageClassName,omitempty"` + VcdStorageProfileName string `json:"vcdStorageProfileName,omitempty"` + UseDeleteReclaimPolicy bool `json:"useDeleteReclaimPolicy,omitempty"` + } `json:"defaultStorageClassOptions,omitempty"` + } `json:"vcdKe,omitempty"` + CapiYaml string `json:"capiYaml,omitempty"` + } `json:"spec,omitempty"` + Status struct { + Cpi struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedEvent string `json:"Detailed Event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"cpi,omitempty"` + Csi struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedDescription string `json:"Detailed Description,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"csi,omitempty"` + VcdKe struct { + State string `json:"state,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedEvent string `json:"Detailed Event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + WorkerId string `json:"workerId,omitempty"` + VcdKeVersion string `json:"vcdKeVersion,omitempty"` + VcdResourceSet []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + } `json:"vcdResourceSet,omitempty"` + HeartbeatString string `json:"heartbeatString,omitempty"` + VcdKeInstanceId string `json:"vcdKeInstanceId,omitempty"` + HeartbeatTimestamp string `json:"heartbeatTimestamp,omitempty"` + DefaultStorageClass struct { + FileSystem string `json:"fileSystem,omitempty"` + K8SStorageClassName string `json:"k8sStorageClassName,omitempty"` + VcdStorageProfileName string `json:"vcdStorageProfileName,omitempty"` + UseDeleteReclaimPolicy bool `json:"useDeleteReclaimPolicy,omitempty"` + } `json:"defaultStorageClass,omitempty"` + } `json:"vcdKe,omitempty"` + Capvcd struct { + Uid string `json:"uid,omitempty"` + Phase string `json:"phase,omitempty"` + // NOTE: "Private" struct needs to be a pointer to avoid overriding with empty values by mistake, as VCD doesn't return RDE fields + // marked with "x-vcloud-restricted: secure" + Private *struct { + KubeConfig string `json:"kubeConfig,omitempty"` + } `json:"private,omitempty"` + Upgrade struct { + Ready bool `json:"ready,omitempty"` + Current struct { + TkgVersion string `json:"tkgVersion,omitempty"` + KubernetesVersion string `json:"kubernetesVersion,omitempty"` + } `json:"current,omitempty"` + } `json:"upgrade,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + NodePool []struct { + Name string `json:"name,omitempty"` + DiskSizeMb int `json:"diskSizeMb,omitempty"` + SizingPolicy string `json:"sizingPolicy,omitempty"` + StorageProfile string `json:"storageProfile,omitempty"` + DesiredReplicas int `json:"desiredReplicas,omitempty"` + AvailableReplicas int `json:"availableReplicas,omitempty"` + } `json:"nodePool,omitempty"` + ParentUid string `json:"parentUid,omitempty"` + K8SNetwork struct { + Pods struct { + CidrBlocks []string `json:"cidrBlocks,omitempty"` + } `json:"pods,omitempty"` + Services struct { + CidrBlocks []string `json:"cidrBlocks,omitempty"` + } `json:"services,omitempty"` + } `json:"k8sNetwork,omitempty"` + Kubernetes string `json:"kubernetes,omitempty"` + CapvcdVersion string `json:"capvcdVersion,omitempty"` + VcdProperties struct { + Site string `json:"site,omitempty"` + OrgVdcs []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + OvdcNetworkName string `json:"ovdcNetworkName,omitempty"` + } `json:"orgVdcs,omitempty"` + Organizations []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } `json:"organizations,omitempty"` + } `json:"vcdProperties,omitempty"` + CapiStatusYaml string `json:"capiStatusYaml,omitempty"` + VcdResourceSet []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + AdditionalDetails struct { + VirtualIP string `json:"virtualIP,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"vcdResourceSet,omitempty"` + ClusterApiStatus struct { + Phase string `json:"phase,omitempty"` + ApiEndpoints []struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + } `json:"apiEndpoints,omitempty"` + } `json:"clusterApiStatus,omitempty"` + CreatedByVersion string `json:"createdByVersion,omitempty"` + ClusterResourceSetBindings []struct { + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Applied bool `json:"applied,omitempty"` + LastAppliedTime string `json:"lastAppliedTime,omitempty"` + ClusterResourceSetName string `json:"clusterResourceSetName,omitempty"` + } `json:"clusterResourceSetBindings,omitempty"` + } `json:"capvcd,omitempty"` + Projector struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + Event string `json:"event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"projector,omitempty"` + } `json:"status,omitempty"` + Metadata struct { + Name string `json:"name,omitempty"` + Site string `json:"site,omitempty"` + OrgName string `json:"orgName,omitempty"` + VirtualDataCenterName string `json:"virtualDataCenterName,omitempty"` + } `json:"metadata,omitempty"` + ApiVersion string `json:"apiVersion,omitempty"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/ip_space.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/ip_space.go index a8305909c..81090c935 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/ip_space.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/ip_space.go @@ -69,10 +69,59 @@ type IpSpace struct { // if the associated Provider Gateway is owned by the Organization. RouteAdvertisementEnabled bool `json:"routeAdvertisementEnabled"` + // DefaultGatewayServiceConfig specifies default gateway services configurations such as NAT and + // Firewall rules that a user can apply on either the Provider Gateway or Edge Gateway depending + // on the network topology. Note that re-applying the default services on the Provider Gateway + // or Edge Gateway may delete/update/create services that are managed/created by VCD. + // + // Requires VCD 10.5.0+ (API v38.0+) + DefaultGatewayServiceConfig *IpSpaceDefaultGatewayServiceConfig `json:"defaultGatewayServiceConfig,omitempty"` + // Status is one of `PENDING`, `CONFIGURING`, `REALIZED`, `REALIZATION_FAILED`, `UNKNOWN` Status string `json:"status,omitempty"` } +// IpSpaceDefaultGatewayServiceConfig specified the default gateway services configurations such as NAT and Firewall rules +// that a user can apply on either the Provider Gateway or Edge Gateway depending on the network +// topology. Below is an example of the ordering of NAT rule: +// * If IP Space's external scope maps to any network such as "0.0.0.0/0", the NO SNAT rules +// priority is 1001 and the default SNAT rules will have priority 1000 +// * All other default SNAT rules has priority 100 +// * All other default NO SNAT rules has priority 0 +// * User-created NAT rules has default priority 50 +// +// Requires VCD 10.5.0+ (API v38.0+) +type IpSpaceDefaultGatewayServiceConfig struct { + // If true, the user can choose to later apply the default firewall rules on either the Provider + // Gateway or Edge Gateway. These firewall rules are created only if the corresponding + // associated default No SNAT and NAT rules are configured. False means that the default + // firewall rules will not be created. + // For the associated default SNAT rule, the source is ANY and the destination is the IP Space's + // external scope. + // For the associated default No SNAT rule, the source is the IP Space's internal scopes and the + // destination is the IP Space's external scope. + EnableDefaultFirewallRuleCreation bool `json:"enableDefaultFirewallRuleCreation,omitempty"` + // If true, the user can choose to later apply the default No SNAT rules on either the Provider + // Gateway or Edge Gateway. + // False means that the default No SNAT rule will not be created. + // An example of a default No NAT rule is that the source CIDR is the IP Space's internal scope + // and the destination CIDR is the IP Space's external scope. This allows traffic to and from + // the IP Space's internal and external scope to not be affected by any NAT rule. An example of + // such traffic is that an Organization VDC Network within IP Space's internal scope will be + // able to route out to the internet. This means that this configuration can allow both + // fully-routed topology and also NAT-routed topology. + EnableDefaultNoSnatRuleCreation bool `json:"enableDefaultNoSnatRuleCreation,omitempty"` + // If true, the user can choose to later apply the default SNAT rules on either the Provider + // Gateway or Edge Gateway. + // False means that the default SNAT rule will not be created. + // An example of a default NAT rule is that the source CIDR is ANY, the destination CIDR is the + // IP Space's external scope. This allows all traffic such as from a private network to be able + // to access the external destination IPs specified by the IP Space's external scope such as the + // internet. Note that the translated external IP will be allocated from this IP Space if there + // are no free ones to be used for the SNAT rules. + EnableDefaultSnatRuleCreation bool `json:"enableDefaultSnatRuleCreation,omitempty"` +} + type FloatingIPs struct { // TotalCount holds the number of IP addresses or IP Prefixes defined by the IP Space. If user // does not own this IP Space, this is the quota that the user's organization is granted. A '-1' @@ -315,3 +364,10 @@ type IpSpaceOrgAssignmentIPPrefixQuotas struct { PrefixLength *int `json:"prefixLength"` Quota *int `json:"quota"` } + +// IpSpaceFloatingIpSuggestion provides a list of unused IP Addresses in an IP Space +type IpSpaceFloatingIpSuggestion struct { + IPSpaceRef OpenApiReference `json:"ipSpaceRef"` + // UnusedValues lists unused IP Addresses or IP Prefixes from the referenced IP Space + UnusedValues []string `json:"unusedValues"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_segment_profiles.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_segment_profiles.go new file mode 100644 index 000000000..f91b07ea7 --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_segment_profiles.go @@ -0,0 +1,207 @@ +package types + +// NsxtSegmentProfileTemplate allows management of templates that define the segment profiles that +// will be applied during network creation. +type NsxtSegmentProfileTemplate struct { + ID string `json:"id,omitempty"` + // Name for Segment Profile template + Name string `json:"name"` + // Description for Segment Profile template + Description string `json:"description,omitempty"` + + // SourceNsxTManagerRef points to NSX-T manager providing the source segment profiles + SourceNsxTManagerRef *OpenApiReference `json:"sourceNsxTManagerRef,omitempty"` + IPDiscoveryProfile *Reference `json:"ipDiscoveryProfile,omitempty"` + MacDiscoveryProfile *Reference `json:"macDiscoveryProfile,omitempty"` + QosProfile *Reference `json:"qosProfile,omitempty"` + SegmentSecurityProfile *Reference `json:"segmentSecurityProfile,omitempty"` + SpoofGuardProfile *Reference `json:"spoofGuardProfile,omitempty"` + + LastModified string `json:"lastModified,omitempty"` +} + +// NsxtSegmentProfileCommonFields contains common fields that are used in all NSX-T Segment +// Profiles +type NsxtSegmentProfileCommonFields struct { + ID string `json:"id,omitempty"` + // Description of the segment profile. + Description string `json:"description,omitempty"` + // DisplayName represents name of the segment profile. This corresponds to the name used in + // NSX-T managers logs or GUI. + DisplayName string `json:"displayName"` + // NsxTManagerRef where this segment profile is configured. + NsxTManagerRef *OpenApiReference `json:"nsxTManagerRef"` +} + +// NsxtSegmentProfileIpDiscovery contains information about NSX-T IP Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileIpDiscovery struct { + NsxtSegmentProfileCommonFields + // ArpBindingLimit indicates the number of arp snooped IP addresses to be remembered per + // LogicalPort. + ArpBindingLimit int `json:"arpBindingLimit"` + // ArpNdBindingTimeout indicates ARP and ND cache timeout (in minutes). + ArpNdBindingTimeout int `json:"arpNdBindingTimeout"` + // IsArpSnoopingEnabled defines whether ARP snooping is enabled. + IsArpSnoopingEnabled bool `json:"isArpSnoopingEnabled"` + // IsDhcpSnoopingV4Enabled defines whether DHCP snooping for IPv4 is enabled. + IsDhcpSnoopingV4Enabled bool `json:"isDhcpSnoopingV4Enabled"` + // IsDhcpSnoopingV6Enabled defines whether DHCP snooping for IPv6 is enabled. + IsDhcpSnoopingV6Enabled bool `json:"isDhcpSnoopingV6Enabled"` + // IsDuplicateIPDetectionEnabled indicates whether duplicate IP detection is enabled. Duplicate + // IP detection is used to determine if there is any IP conflict with any other port on the same + // logical switch. If a conflict is detected, then the IP is marked as a duplicate on the port + // where the IP was discovered last. + IsDuplicateIPDetectionEnabled bool `json:"isDuplicateIpDetectionEnabled"` + // IsNdSnoopingEnabled indicates whether ND snooping is enabled. If true, this method will snoop + // the NS (Neighbor Solicitation) and NA (Neighbor Advertisement) messages in the ND (Neighbor + // Discovery Protocol) family of messages which are transmitted by a VM. From the NS messages, + // we will learn about the source which sent this NS message. From the NA message, we will learn + // the resolved address in the message which the VM is a recipient of. Addresses snooped by this + // method are subject to TOFU. + IsNdSnoopingEnabled bool `json:"isNdSnoopingEnabled"` + // IsTofuEnabled defines whether 'Trust on First Use(TOFU)' paradigm is enabled. + IsTofuEnabled bool `json:"isTofuEnabled"` + // IsVMToolsV4Enabled indicates whether fetching IPv4 address using vm-tools is enabled. This + // option is only supported on ESX where vm-tools is installed. + IsVMToolsV4Enabled bool `json:"isVmToolsV4Enabled"` + // IsVMToolsV6Enabled indicates whether fetching IPv6 address using vm-tools is enabled. This + // will learn the IPv6 addresses which are configured on interfaces of a VM with the help of the + // VMTools software. + IsVMToolsV6Enabled bool `json:"isVmToolsV6Enabled"` + // NdSnoopingLimit defines maximum number of ND (Neighbor Discovery Protocol) snooped IPv6 + // addresses. + NdSnoopingLimit int `json:"ndSnoopingLimit"` +} + +// NsxtSegmentProfileMacDiscovery contains information about NSX-T MAC Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileMacDiscovery struct { + NsxtSegmentProfileCommonFields + // IsMacChangeEnabled indcates whether source MAC address change is enabled. + IsMacChangeEnabled bool `json:"isMacChangeEnabled"` + // IsMacLearningEnabled indicates whether source MAC address learning is enabled. + IsMacLearningEnabled bool `json:"isMacLearningEnabled"` + // IsUnknownUnicastFloodingEnabled indicates whether unknown unicast flooding rule is enabled. + // This allows flooding for unlearned MAC for ingress traffic. + IsUnknownUnicastFloodingEnabled bool `json:"isUnknownUnicastFloodingEnabled"` + // MacLearningAgingTime indicates aging time in seconds for learned MAC address. Indicates how + // long learned MAC address remain. + MacLearningAgingTime int `json:"macLearningAgingTime"` + // MacLimit indicates the maximum number of MAC addresses that can be learned on this port. + MacLimit int `json:"macLimit"` + // MacPolicy defines the policy after MAC Limit is exceeded. It can be either 'ALLOW' or 'DROP'. + MacPolicy string `json:"macPolicy"` +} + +// NsxtSegmentProfileSegmentSpoofGuard contains information about NSX-T Spoof Guard Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentSpoofGuard struct { + NsxtSegmentProfileCommonFields + // IsAddressBindingWhitelistEnabled indicates whether Spoof Guard is enabled. If true, it only + // allows VM sending traffic with the IPs in the whitelist. + IsAddressBindingWhitelistEnabled bool `json:"isAddressBindingWhitelistEnabled"` +} + +// NsxtSegmentProfileSegmentQosProfile contains information about NSX-T QoS Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentQosProfile struct { + NsxtSegmentProfileCommonFields + // ClassOfService groups similar types of traffic in the network and each type of traffic is + // treated as a class with its own level of service priority. The lower priority traffic is + // slowed down or in some cases dropped to provide better throughput for higher priority + // traffic. + ClassOfService int `json:"classOfService"` + // DscpConfig contains a Differentiated Services Code Point (DSCP) Configuration for this + // Segment QoS Profile. + DscpConfig struct { + Priority int `json:"priority"` + TrustMode string `json:"trustMode"` + } `json:"dscpConfig"` + // EgressRateLimiter indicates egress rate properties in Mb/s. + EgressRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"egressRateLimiter"` + // IngressBroadcastRateLimiter indicates broadcast rate properties in Mb/s. + IngressBroadcastRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"ingressBroadcastRateLimiter"` + // IngressRateLimiter indicates ingress rate properties in Mb/s. + IngressRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"ingressRateLimiter"` +} + +// NsxtSegmentProfileIpDiscovery contains information about NSX-T IP Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentQosProfileRateLimiter struct { + // Average bandwidth in Mb/s. + AvgBandwidth int `json:"avgBandwidth"` + // Burst size in bytes. + BurstSize int `json:"burstSize"` + // Peak bandwidth in Mb/s. + PeakBandwidth int `json:"peakBandwidth"` +} + +// NsxtSegmentProfileSegmentSecurity contains information about NSX-T Segment Security Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentSecurity struct { + NsxtSegmentProfileCommonFields + // BpduFilterAllowList indicates pre-defined list of allowed MAC addresses to be excluded from + // BPDU filtering. + BpduFilterAllowList []string `json:"bpduFilterAllowList"` + // IsBpduFilterEnabled indicates whether BPDU filter is enabled. + IsBpduFilterEnabled bool `json:"isBpduFilterEnabled"` + // IsDhcpClientBlockV4Enabled indicates whether DHCP Client block IPv4 is enabled. This filters + // DHCP Client IPv4 traffic. + IsDhcpClientBlockV4Enabled bool `json:"isDhcpClientBlockV4Enabled"` + // IsDhcpClientBlockV6Enabled indicates whether DHCP Client block IPv6 is enabled. This filters + // DHCP Client IPv4 traffic. + IsDhcpClientBlockV6Enabled bool `json:"isDhcpClientBlockV6Enabled"` + // IsDhcpServerBlockV4Enabled indicates whether DHCP Server block IPv4 is enabled. This filters + // DHCP Server IPv4 traffic. + IsDhcpServerBlockV4Enabled bool `json:"isDhcpServerBlockV4Enabled"` + // IsDhcpServerBlockV6Enabled indicates whether DHCP Server block IPv6 is enabled. This filters + // DHCP Server IPv6 traffic. + IsDhcpServerBlockV6Enabled bool `json:"isDhcpServerBlockV6Enabled"` + // IsNonIPTrafficBlockEnabled indicates whether non IP traffic block is enabled. If true, it + // blocks all traffic except IP/(G)ARP/BPDU. + IsNonIPTrafficBlockEnabled bool `json:"isNonIpTrafficBlockEnabled"` + // IsRaGuardEnabled indicates whether Router Advertisement Guard is enabled. This filters DHCP + // Server IPv6 traffic. + IsRaGuardEnabled bool `json:"isRaGuardEnabled"` + // IsRateLimitingEnabled indicates whether Rate Limiting is enabled. + IsRateLimitingEnabled bool `json:"isRateLimitingEnabled"` + RateLimits struct { + // Incoming broadcast traffic limit in packets per second. + RxBroadcast int `json:"rxBroadcast"` + // Incoming multicast traffic limit in packets per second. + RxMulticast int `json:"rxMulticast"` + // Outgoing broadcast traffic limit in packets per second. + TxBroadcast int `json:"txBroadcast"` + // Outgoing multicast traffic limit in packets per second. + TxMulticast int `json:"txMulticast"` + } `json:"rateLimits"` +} + +// NsxtGlobalDefaultSegmentProfileTemplate is a structure that sets VCD global default Segment +// Profile Templates +type NsxtGlobalDefaultSegmentProfileTemplate struct { + VappNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vappNetworkSegmentProfileTemplateRef,omitempty"` + VdcNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vdcNetworkSegmentProfileTemplateRef,omitempty"` +} + +// OrgVdcNetworkSegmentProfiles defines Segment Profile configuration structure for Org VDC networks +// An Org VDC network may have a Segment Profile Template assigned, or individual Segment Profiles +type OrgVdcNetworkSegmentProfiles struct { + // SegmentProfileTemplate contains a read-only reference to Segment Profile Template + // To update Segment Profile Template for a particular Org VDC network, one must use + // `OpenApiOrgVdcNetwork.SegmentProfileTemplate` field and `OpenApiOrgVdcNetwork.Update()` + SegmentProfileTemplate *SegmentProfileTemplateRef `json:"segmentProfileTemplate,omitempty"` + + IPDiscoveryProfile *Reference `json:"ipDiscoveryProfile"` + MacDiscoveryProfile *Reference `json:"macDiscoveryProfile"` + QosProfile *Reference `json:"qosProfile"` + SegmentSecurityProfile *Reference `json:"segmentSecurityProfile"` + SpoofGuardProfile *Reference `json:"spoofGuardProfile"` +} + +// SegmentProfileTemplateRef contains reference to segment profile +type SegmentProfileTemplateRef struct { + Source string `json:"source"` + TemplateRef *OpenApiReference `json:"templateRef"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_types.go index 5be960213..7dcd9a4e0 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/nsxt_types.go @@ -72,6 +72,15 @@ type EdgeGatewayUplinks struct { // Advertisement for this Edge Gateway Dedicated bool `json:"dedicated,omitempty"` + // BackingType of uplink. Exactly one of these will be present: + // * types.ExternalNetworkBackingTypeNsxtTier0Router (NSXT_TIER0) + // * types.ExternalNetworkBackingTypeNsxtVrfTier0Router (NSXT_VRF_TIER0) + // Additional External network uplinks + // * types.ExternalNetworkBackingTypeNsxtSegment (IMPORTED_T_LOGICAL_SWITCH) - External Network uplinks (External Networks backed by NSX-T Segment) + // + // Note. VCD 10.4.1+. Not mandatory to set it - can be used as a read only value + BackingType *string `json:"backingType,omitempty"` + // UsingIpSpace is a boolean flag showing if the uplink uses IP Space UsingIpSpace *bool `json:"usingIpSpace,omitempty"` } @@ -109,7 +118,7 @@ type OpenAPIEdgeGatewaySubnetValue struct { // UsedIPCount specifies used IP count UsedIPCount int `json:"usedIpCount,omitempty"` - // PrimaryIP of the Edge Gateway. Can only be one per Edge (from all subnets) + // PrimaryIP of the Edge Gateway. Can only be one in all subnets of a single uplink PrimaryIP string `json:"primaryIp,omitempty"` // AutoAllocateIPRanges provides a way to automatically allocate @@ -207,6 +216,19 @@ type OpenApiOrgVdcNetwork struct { // EnableDualSubnetNetwork defines whether or not this network will support two subnets (IPv4 // and IPv6) EnableDualSubnetNetwork *bool `json:"enableDualSubnetNetwork,omitempty"` + + // SegmentProfileTemplate reference to the Segment Profile Template that is to be used when + // creating/updating this network. Setting this will override any Org VDC Network Segment + // Profile Template defined at global level or an Org VDC level. + // + // Notes: + // * This field is only relevant during network create/update operation and will not be returned + // on GETs. To retrieve currently set Segment Profile Template one can use different endpoint + // and function `vdc.GetVdcNetworkProfile()` + // * For specific profile types where there are no corresponding profiles defined in the + // template, VCD will use the default NSX-T profile. + // * This field is only applicable for NSX-T Org VDC Networks. + SegmentProfileTemplate *OpenApiReference `json:"segmentProfileTemplateRef,omitempty"` } // OrgVdcNetworkSubnetIPRanges is a type alias to reuse the same definitions with appropriate names @@ -849,6 +871,88 @@ type NsxtIpSecVpnTunnelProfileDpdConfiguration struct { ProbeInterval int `json:"probeInterval"` } +// NsxtL2VpnTunnel defines the L2 VPN Tunnel configuration +type NsxtL2VpnTunnel struct { + // ID of the tunnel + ID string `json:"id"` + // Name and description of the tunnel + Name string `json:"name"` + Description string `json:"description"` + + // The current session mode, one of either SERVER or CLIENT. + // * SERVER - In which the edge gateway acts as the server side of the L2 VPN tunnel and generates peer codes to distribute to client sessions. + // * CLIENT - In which the edge gateway receives peer codes from the server side of the L2 VPN tunnel to establish a connection. + SessionMode string `json:"sessionMode"` + + // State of the tunnel. Default is true + Enabled bool `json:"enabled"` + + // The IP address of the local endpoint, which corresponds to the Edge Gateway the tunnel is being configured on. + LocalEndpointIp string `json:"localEndpointIP"` + + // The IP address of the remote endpoint, which corresponds to the device on the remote site terminating the VPN tunnel. + RemoteEndpointIp string `json:"remoteEndpointIP"` + + // The network CIDR block over which the session interfaces. Relevant only + // for SERVER session modes. If provided, the underlying IPSec tunnel will + // use the specified tunnel interface. If not provided, Cloud Director will + // attempt to automatically allocate a tunnel interface. + TunnelInterface string `json:"tunnelInterface,omitempty"` + + // This is the mode used by the local endpoint to establish an IKE Connection with the remote site. The default is INITIATOR. + // * INITIATOR - Local endpoint initiates tunnel setup and will also respond to incoming tunnel setup requests from the peer gateway. + // * RESPOND_ONLY - Local endpoint shall only respond to incoming tunnel setup requests, it shall not initiate the tunnel setup. + // * ON_DEMAND - In this mode local endpoint will initiate tunnel creation once first packet matching the policy rule is received, and will also respond to incoming initiation requests. + ConnectorInitiationMode string `json:"connectorInitiationMode"` + + // This property is a base64 encoded string of the full configuration for the tunnel, + // generated by the server-side L2 VPN session. An L2 VPN client session must receive + // and validate this string in order to successfully establish a tunnel, but be careful + // sharing or storing this code since it does contain the encoded PSK. + // Leave this property blank if this call is being used to establish a server-side session. + PeerCode string `json:"peerCode"` + + // This is the Pre-shared key used for authentication, no specific format is required. Relevant only for SERVER session modes. + PreSharedKey string `json:"preSharedKey"` + + // The list of OrgVDC Network entity references which are currently attached to this L2VPN tunnel. + StretchedNetworks []EdgeL2VpnStretchedNetwork `json:"stretchedNetworks,omitempty"` + + // Whether logging for the tunnel is enabled or not. + Logging bool `json:"logging"` + + // Version of the entity, needs to be provided on tunnel update calls, can be retrieved by getting the tunnel. + Version VersionField `json:"version"` +} + +// EdgeL2VpnStretchedNetwork specifies the Org vDC network that is stretched through the given L2 VPN Tunnel. +type EdgeL2VpnStretchedNetwork struct { + NetworkRef OpenApiReference `json:"networkRef"` + TunnelID int `json:"tunnelId"` +} + +// EdgeL2VpnTunnelStatistics specifies the statistics for the given L2 VPN Tunnel. +type EdgeL2VpnTunnelStatistics struct { + BumBytesIn int `json:"bumBytesIn"` + BumBytesOut int `json:"bumBytesOut"` + BumPacketsIn int `json:"bumPacketsIn"` + BumPacketsOut int `json:"bumPacketsOut"` + BytesIn int `json:"bytesIn"` + BytesOut int `json:"bytesOut"` + PacketsIn int `json:"packetsIn"` + PacketsOut int `json:"packetsOut"` + PacketsReceiveError int `json:"packetsReceiveError"` + PacketsSentError int `json:"packetsSentError"` + SegmentPath string `json:"segmentPath"` +} + +// EdgeL2VpnTunnelStatus includes the L2 VPN tunnel status such as whether +// the tunnel is up or down and the IKE Session status +type EdgeL2VpnTunnelStatus struct { + FailureReason string `json:"failureReason"` + RuntimeStatus string `json:"runtimeStatus"` +} + // NsxtAlbController helps to integrate VMware Cloud Director with NSX-T Advanced Load Balancer deployment. // Controller instances are registered with VMware Cloud Director instance. Controller instances serve as a central // control plane for the load-balancing services provided by NSX-T Advanced Load Balancer. @@ -1136,6 +1240,9 @@ type NsxtAlbPool struct { // VirtualServiceRefs holds list of Load Balancer Virtual Services associated with this Load balancer Pool. VirtualServiceRefs []OpenApiReference `json:"virtualServiceRefs,omitempty"` + + // SslEnabled is required when CA Certificates are used starting with API V37.0 + SslEnabled *bool `json:"sslEnabled,omitempty"` } // NsxtAlbPoolHealthMonitor checks member servers health. Active monitor generates synthetic traffic and mark a server @@ -1782,3 +1889,67 @@ type NsxtEdgeGatewayStaticRouteNextHopScope struct { // * SYSTEM_OWNED ScopeType string `json:"scopeType"` } + +// NsxtManager structure for reading NSX-T Manager +type NsxtManager struct { + OtherAttributes struct { + } `json:"otherAttributes"` + Link LinkList `json:"link"` + Href string `json:"href"` + Type string `json:"type"` + ID string `json:"id"` + OperationKey interface{} `json:"operationKey"` + Description string `json:"description"` + Tasks interface{} `json:"tasks"` + Name string `json:"name"` + Username string `json:"username"` + Password interface{} `json:"password"` + URL string `json:"url"` + DeploymentType string `json:"deploymentType"` + VirtualCenterReference interface{} `json:"virtualCenterReference"` + ServiceAppReference interface{} `json:"serviceAppReference"` + NetworkProviderScope interface{} `json:"networkProviderScope"` + Version string `json:"version"` + ProxyConfigurationReference interface{} `json:"proxyConfigurationReference"` + GlobalManager bool `json:"globalManager"` + LocalNsxTManagerRef []interface{} `json:"localNsxTManagerRef"` + LocalManagerLocationName interface{} `json:"localManagerLocationName"` + VCloudExtension []interface{} `json:"vCloudExtension"` +} + +// NsxtEdgeGatewayDns is used for configuring the DNS forwarder for a specific Edge Gateway +type NsxtEdgeGatewayDns struct { + // Status of the DNS forwarder + Enabled bool `json:"enabled"` + // The IP on which the DNS forwarder listens. If the Edge Gateway has a dedicated + // external network, this can be changed. + ListenerIp string `json:"listenerIp,omitempty"` + // Whether an SNAT rule exists for the DNS forwarder or not. In NAT + // routed environments, an SNAT rule is required for the Edge DNS forwarder + // to send traffic to an upstream server. In fully routed environments, + // this is not needed if the listener IP is on an advertised subnet. + SnatRuleEnabled bool `json:"snatRuleEnabled,omitempty"` + // The external IP address of the SNAT rule. This property only applies if the + // Edge Gateway is connected to a Provider Gateway using IP Spaces. + SnatRuleExternalIpAddress string `json:"snatRuleExternalIpAddress,omitempty"` + // The default forwarder zone to use if there’s no matching domain in the conditional forwarder zone. + DefaultForwarderZone *NsxtDnsForwarderZoneConfig `json:"defaultForwarderZone,omitempty"` + // The list of forwarder zones with its matching DNS domains. + ConditionalForwarderZones []*NsxtDnsForwarderZoneConfig `json:"conditionalForwarderZones,omitempty"` + Version *VersionField `json:"version,omitempty"` +} + +type NsxtDnsForwarderZoneConfig struct { + // The unique id of the DNS forwarder zone. If value is set, + // the zone is updated; if not, a new zone is created. + ID string `json:"id,omitempty"` + // User friendly name for the zone + DisplayName string `json:"displayName,omitempty"` + // DNS servers to which the DNS request needs to be forwarded. + UpstreamServers []string `json:"upstreamServers,omitempty"` + // List of domain names on which conditional forwarding is based. This + // field is required if the DNS Zone is being used for a conditional forwarder. + // This field will also be used for conditional reverse lookup. This field should + // not be set if the zone is used as default forwarder zone. + DnsDomainNames []string `json:"dnsDomainNames,omitempty"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/openapi.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/openapi.go index 666ff8b16..d738856a8 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/openapi.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/openapi.go @@ -221,6 +221,8 @@ type VdcComputePolicyV2 struct { IsVgpuPolicy bool `json:"isVgpuPolicy,omitempty"` PvdcNamedVmGroupsMap []PvdcNamedVmGroupsMap `json:"pvdcNamedVmGroupsMap,omitempty"` PvdcLogicalVmGroupsMap []PvdcLogicalVmGroupsMap `json:"pvdcLogicalVmGroupsMap,omitempty"` + PvdcVgpuClustersMap []PvdcVgpuClustersMap `json:"pvdcVgpuClustersMap,omitempty"` + VgpuProfiles []VgpuProfile `json:"vgpuProfiles,omitempty"` } // PvdcNamedVmGroupsMap is a combination of a reference to a Provider VDC and a list of references to Named VM Groups. @@ -237,6 +239,11 @@ type PvdcLogicalVmGroupsMap struct { Pvdc OpenApiReference `json:"pvdc,omitempty"` } +type PvdcVgpuClustersMap struct { + Clusters []string `json:"clusters,omitempty"` + Pvdc OpenApiReference `json:"pvdc,omitempty"` +} + // OpenApiReference is a generic reference type commonly used throughout OpenAPI endpoints type OpenApiReference struct { Name string `json:"name,omitempty"` @@ -573,24 +580,24 @@ type OpenApiSupportedHardwareVersions struct { // NetworkPool is the full data retrieved for a provider network pool type NetworkPool struct { - Status string `json:"status"` - Id string `json:"id"` + Status string `json:"status,omitempty"` + Id string `json:"id,omitempty"` Name string `json:"name"` Description string `json:"description"` PoolType string `json:"poolType"` - PromiscuousMode bool `json:"promiscuousMode"` - TotalBackingsCount int `json:"totalBackingsCount"` - UsedBackingsCount int `json:"usedBackingsCount"` + PromiscuousMode bool `json:"promiscuousMode,omitempty"` + TotalBackingsCount int `json:"totalBackingsCount,omitempty"` + UsedBackingsCount int `json:"usedBackingsCount,omitempty"` ManagingOwnerRef OpenApiReference `json:"managingOwnerRef"` Backing NetworkPoolBacking `json:"backing"` } // NetworkPoolBacking is the definition of the objects supporting the network pool type NetworkPoolBacking struct { - VlanIdRanges VlanIdRanges `json:"vlanIdRanges"` - VdsRefs []OpenApiReference `json:"vdsRefs"` - PortGroupRefs []OpenApiReference `json:"portGroupRefs"` - TransportZoneRef OpenApiReference `json:"transportZoneRef"` + VlanIdRanges VlanIdRanges `json:"vlanIdRanges,omitempty"` + VdsRefs []OpenApiReference `json:"vdsRefs,omitempty"` + PortGroupRefs []OpenApiReference `json:"portGroupRefs,omitempty"` + TransportZoneRef OpenApiReference `json:"transportZoneRef,omitempty"` ProviderRef OpenApiReference `json:"providerRef"` } @@ -598,6 +605,7 @@ type VlanIdRanges struct { Values []VlanIdRange `json:"values"` } +// VlanIdRange is a component of a network pool of type VLAN type VlanIdRange struct { StartId int `json:"startId"` EndId int `json:"endId"` @@ -631,3 +639,54 @@ type UploadSpec struct { Checksum string `json:"checksum,omitempty"` ChecksumAlgo string `json:"checksumAlgo,omitempty"` } + +type TransportZones struct { + Values []*TransportZone `json:"values"` +} + +// TransportZone is a backing component of a network pool of type 'GENEVE' (NSX-T backed) +type TransportZone struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type,omitempty"` + AlreadyImported bool `json:"alreadyImported"` +} + +// VcenterDistributedSwitch is a backing component of a network pool of type VLAN +type VcenterDistributedSwitch struct { + BackingRef OpenApiReference `json:"backingRef"` + VirtualCenter OpenApiReference `json:"virtualCenter"` +} + +// OpenApiMetadataEntry represents a metadata entry in VCD. +type OpenApiMetadataEntry struct { + ID string `json:"id,omitempty"` // UUID for OpenApiMetadataEntry. This is immutable + IsPersistent bool `json:"persistent,omitempty"` // Persistent entries can be copied over on some entity operation, for example: Creating a copy of an Org VDC, capturing a vApp to a template, instantiating a catalog item as a VM, etc. + IsReadOnly bool `json:"readOnly,omitempty"` // The kind of level of access organizations of the entry’s domain have + KeyValue OpenApiMetadataKeyValue `json:"keyValue,omitempty"` // Contains core metadata entry data +} + +// OpenApiMetadataKeyValue contains core metadata entry data. +type OpenApiMetadataKeyValue struct { + Domain string `json:"domain,omitempty"` // Only meaningful for providers. Allows them to share entries with their tenants. Currently, accepted values are: `TENANT`, `PROVIDER`, where that is the ascending sort order of the enumeration. + Key string `json:"key,omitempty"` // Key of the metadata entry + Value OpenApiMetadataTypedValue `json:"value,omitempty"` // Value of the metadata entry + Namespace string `json:"namespace,omitempty"` // Namespace of the metadata entry +} + +// OpenApiMetadataTypedValue the type and value of the metadata entry. +type OpenApiMetadataTypedValue struct { + Value interface{} `json:"value,omitempty"` // The Value is anything because it depends on the Type field. + Type string `json:"type,omitempty"` +} + +// VgpuProfile uniquely represents a type of vGPU +// vGPU Profiles are fetched from your NVIDIA GRID GPU enabled Clusters in vCenter. +type VgpuProfile struct { + Id string `json:"id"` + Name string `json:"name"` + TenantFacingName string `json:"tenantFacingName"` + Instructions string `json:"instructions,omitempty"` + AllowMultiplePerVm bool `json:"allowMultiplePerVm"` + Count int `json:"count,omitempty"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go index ab2e72a3f..530daf3a8 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go @@ -583,6 +583,7 @@ type VdcStorageProfileConfiguration struct { // https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/types/VdcStorageProfileType.html // https://vdc-repo.vmware.com/vmwb-repository/dcr-public/71e12563-bc11-4d64-821d-92d30f8fcfa1/7424bf8e-aec2-44ad-be7d-b98feda7bae0/doc/doc/types/AdminVdcStorageProfileType.html type VdcStorageProfile struct { + ID string `xml:"id,attr"` Xmlns string `xml:"xmlns,attr"` Name string `xml:"name,attr"` Enabled *bool `xml:"Enabled,omitempty"` @@ -941,8 +942,8 @@ type OrgLdapSettingsType struct { Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. - CustomUsersOu string `xml:"CustomUsersOu,omitempty"` // If OrgLdapMode is SYSTEM, specifies an LDAP attribute=value pair to use for OU (organizational unit). OrgLdapMode string `xml:"OrgLdapMode,omitempty"` // LDAP mode you want + CustomUsersOu string `xml:"CustomUsersOu,omitempty"` // If OrgLdapMode is SYSTEM, specifies an LDAP attribute=value pair to use for OU (organizational unit). CustomOrgLdapSettings *CustomOrgLdapSettings `xml:"CustomOrgLdapSettings,omitempty"` // Needs to be set if user chooses custom mode } @@ -1134,6 +1135,14 @@ type PublishExternalCatalogParams struct { PreserveIdentityInfoFlag *bool `xml:"PreserveIdentityInfoFlag,omitempty"` // True includes BIOS UUIDs and MAC addresses in the downloaded OVF package. If false, those information will be excluded. } +// PublishCatalogParams represents the configuration parameters of a catalog published to other orgs. +// It is used in conjunction with the "IsPublished" state of the catalog itself +type PublishCatalogParams struct { + XMLName xml.Name `xml:"PublishCatalogParams"` + Xmlns string `xml:"xmlns,attr,omitempty"` + IsPublished *bool `xml:"IsPublished,omitempty"` // True enables publication (read-only access) +} + // ExternalCatalogSubscription represents the configuration parameters for a catalog that has an external subscription // Type: ExternalCatalogSubscriptionParamsType // Namespace: http://www.vmware.com/vcloud/v1.5 @@ -1573,6 +1582,7 @@ type VAppTemplate struct { NetworkConnectionSection *NetworkConnectionSection `xml:"NetworkConnectionSection,omitempty"` LeaseSettingsSection *LeaseSettingsSection `xml:"LeaseSettingsSection,omitempty"` CustomizationSection *CustomizationSection `xml:"CustomizationSection,omitempty"` + ProductSection *ProductSection `xml:"ProductSection,omitempty"` // OVF Section needs to be added // Section Section `xml:"Section,omitempty"` } @@ -1596,6 +1606,46 @@ type VAppTemplateForUpdate struct { Description string `xml:"Description,omitempty"` // Optional description. } +// CaptureVAppParams is a configuration that can be supplied for capturing a vApp template from +// existing vApp +type CaptureVAppParams struct { + XMLName xml.Name `xml:"CaptureVAppParams"` + + Xmlns string `xml:"xmlns,attr"` + XmlnsNs0 string `xml:"xmlns:ns0,attr,omitempty"` + + // Name of vApp template + Name string `xml:"name,attr"` + // Description of vApp template + Description string `xml:"Description,omitempty"` + + // Source vApp reference. At least HREF field must be set + Source *Reference `xml:"Source"` + + // CustomizationSection section + CustomizationSection CaptureVAppParamsCustomizationSection `xml:"CustomizationSection"` + + // TargetCatalogItem can be used to overwrite existing item. To overwrite an existing vApp + // template with the one created by this capture, place a reference to the existing template + // here. Otherwise, the operation creates a new vApp template. + TargetCatalogItem *Reference `xml:"TargetCatalogItem,omitempty"` + + // CopyTpmOnInstantiate defines if TPM device is copied (`true`) to instantiated vApp from this + // template or `false` if a new TPM device is created for instantiated vApp. + // Note. Supported on VCD 10.4.2+ + CopyTpmOnInstantiate *bool `xml:"CopyTpmOnInstantiate"` +} + +// CaptureVAppParamsCustomizationSection settings for CaptureVAppParams type +type CaptureVAppParamsCustomizationSection struct { + // This field must contain value "CustomizeOnInstantiate Settings" so that API does not reject + // the request + Info string `xml:"ns0:Info,omitempty"` + // CustomizeOnInstantiate marks if instantiating this template applies customization settings + // (`true`). `false` creates an identical copy. + CustomizeOnInstantiate bool `xml:"CustomizeOnInstantiate"` +} + // VMDiskChange represents a virtual machine only with Disk setting update part type VMDiskChange struct { XMLName xml.Name `xml:"Vm"` @@ -1619,18 +1669,25 @@ type DiskSection struct { // DiskSettings from Vm/VmSpecSection/DiskSection struct type DiskSettings struct { - DiskId string `xml:"DiskId,omitempty"` // Specifies a unique identifier for this disk in the scope of the corresponding VM. This element is optional when creating a VM, but if it is provided it should be unique. This element is mandatory when updating an existing disk. - SizeMb int64 `xml:"SizeMb"` // The size of the disk in MB. - UnitNumber int `xml:"UnitNumber"` // The device number on the SCSI or IDE controller of the disk. - BusNumber int `xml:"BusNumber"` // The number of the SCSI or IDE controller itself. - AdapterType string `xml:"AdapterType"` // The type of disk controller, e.g. IDE vs SCSI and if SCSI bus-logic vs LSI logic. - ThinProvisioned *bool `xml:"ThinProvisioned,omitempty"` // Specifies whether the disk storage is pre-allocated or allocated on demand. - Disk *Reference `xml:"Disk,omitempty"` // Specifies reference to a named disk. - StorageProfile *Reference `xml:"StorageProfile,omitempty"` // Specifies reference to a storage profile to be associated with the disk. - OverrideVmDefault bool `xml:"overrideVmDefault"` // Specifies that the disk storage profile overrides the VM's default storage profile. - Iops *int64 `xml:"iops,omitempty"` // Specifies the IOPS for the disk. - VirtualQuantity *int64 `xml:"VirtualQuantity,omitempty"` // The actual size of the disk. - VirtualQuantityUnit string `xml:"VirtualQuantityUnit,omitempty"` // The units in which VirtualQuantity is measured. + DiskId string `xml:"DiskId,omitempty"` // Specifies a unique identifier for this disk in the scope of the corresponding VM. This element is optional when creating a VM, but if it is provided it should be unique. This element is mandatory when updating an existing disk. + SizeMb int64 `xml:"SizeMb"` // The size of the disk in MB. + UnitNumber int `xml:"UnitNumber"` // The device number on the SCSI or IDE controller of the disk. + BusNumber int `xml:"BusNumber"` // The number of the SCSI or IDE controller itself. + AdapterType string `xml:"AdapterType"` // The type of disk controller, e.g. IDE vs SCSI and if SCSI bus-logic vs LSI logic. + ThinProvisioned *bool `xml:"ThinProvisioned,omitempty"` // Specifies whether the disk storage is pre-allocated or allocated on demand. + Disk *Reference `xml:"Disk,omitempty"` // Specifies reference to a named disk. + StorageProfile *Reference `xml:"StorageProfile,omitempty"` // Specifies reference to a storage profile to be associated with the disk. + OverrideVmDefault bool `xml:"overrideVmDefault"` // Specifies that the disk storage profile overrides the VM's default storage profile. + IopsAllocation *IopsResource `xml:"IopsAllocation"` // IOPS definition for the disk - added in 10.4 in replacement of 'iops' + VirtualQuantity *int64 `xml:"VirtualQuantity,omitempty"` // The actual size of the disk. + VirtualQuantityUnit string `xml:"VirtualQuantityUnit,omitempty"` // The units in which VirtualQuantity is measured. +} + +type IopsResource struct { + Reservation int64 `xml:"Reservation"` // The amount of reservation of IOPS on the underlying virtualization infrastructure. This is a read-only. + Limit int64 `xml:"Limit"` // The limit for how much of IOPS can be consumed on the underlying virtualization infrastructure. This is only valid when the resource allocation is not unlimited. + SharesLevel string `xml:"SharesLevel"` // LOW - NORMAL - HIGH - CUSTOM + Shares int64 `xml:"Shares"` // Custom priority for IOPS. This is a read-only. } // MediaSection from Vm/VmSpecSection struct @@ -2362,6 +2419,8 @@ type QueryResultRecordsType struct { VmGroupsRecord []*QueryResultVmGroupsRecordType `xml:"VmGroupsRecord"` // A record representing a VM Group TaskRecord []*QueryResultTaskRecordType `xml:"TaskRecord"` // A record representing a Task AdminTaskRecord []*QueryResultTaskRecordType `xml:"AdminTaskRecord"` // A record representing an Admin Task + VappNetworkRecord []*QueryResultVappNetworkRecordType `xml:"VAppNetworkRecord"` // A record representing a vApp network + AdminVappNetworkRecord []*QueryResultVappNetworkRecordType `xml:"AdminVAppNetworkRecord"` // A record representing an admin vApp network } // QueryResultVmGroupsRecordType represent a VM Groups record @@ -2375,6 +2434,32 @@ type QueryResultVmGroupsRecordType struct { NamedVmGroupId string `xml:"namedVmGroupId,attr,omitempty"` } +type QueryResultVappNetworkRecordType struct { + HREF string `xml:"href,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Type string `xml:"linkType,attr,omitempty"` + IpScopeId string `xml:"ipScopeId,attr,omitempty"` + IpScopeInherited bool `xml:"ipScopeInherited,attr,omitempty"` + Gateway string `xml:"gateway,attr,omitempty"` + Netmask string `xml:"netmask,attr,omitempty"` + SubnetPrefixLength int `xml:"subnetPrefixLength,attr,omitempty"` + Dns1 string `xml:"dns1,attr,omitempty"` + Dns2 string `xml:"dns2,attr,omitempty"` + DnsSuffix string `xml:"dnsSuffix,attr,omitempty"` + Vapp string `xml:"vApp,attr,omitempty"` // the HREF of the parent vApp + VappName string `xml:"vAppName,attr,omitempty"` // the name of the parent vApp + LinkNetworkName string `xml:"linkNetworkName,attr,omitempty"` // this field is filled when called in tenant context + RealNetworkName string `xml:"realNetworkName,attr,omitempty"` + RealNetworkPortgroupId string `xml:"realNetworkPortgroupId,attr,omitempty"` + VCenterName string `xml:"vcName,attr,omitempty"` + VCenter string `xml:"vc,attr,omitempty"` + IsBusy bool `xml:"isBusy,attr,omitempty"` + IsLinked bool `xml:"isLinked,attr,omitempty"` + RetainNicResources bool `xml:"retainNicResources,attr,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` +} + // QueryResultResourcePoolRecordType represent a Resource Pool record type QueryResultResourcePoolRecordType struct { HREF string `xml:"href,attr,omitempty"` @@ -2549,6 +2634,7 @@ type QueryResultVMRecordType struct { OwnerName string `xml:"ownerName,attr,omitempty"` Owner string `xml:"owner,attr,omitempty"` VdcHREF string `xml:"vdc,attr,omitempty"` + VdcName string `xml:"vdcName,attr,omitempty"` VAppTemplate bool `xml:"isVAppTemplate,attr,omitempty"` Deleted bool `xml:"isDeleted,attr,omitempty"` GuestOS string `xml:"guestOs,attr,omitempty"` @@ -3387,3 +3473,97 @@ type EnableStorageProfile struct { type RemoveStorageProfile struct { RemoveStorageProfile []*Reference `json:"removeStorageProfile"` } + +// VirtualHardwareVersion describes supported hardware by the VMs created on the VDC +type VirtualHardwareVersion struct { + HardDiskAdapter []*HardDiskAdapter `xml:"HardDiskAdapter"` + Link Link `xml:"Link"` + MaxCPUs int `xml:"maxCPUs"` + MaxCoresPerSocket int `xml:"maxCoresPerSocket"` + MaxMemorySizeMb int `xml:"maxMemorySizeMb"` + MaxNICs int `xml:"maxNICs"` + Name string `xml:"name"` + SupportedMemorySizeGb []int `xml:"supportedMemorySizeGb"` + SupportedCoresPerSocket []int `xml:"supportedCoresPerSocket"` + SupportedOperatingSystems *SupportedOperatingSystemsInfoType `xml:"supportedOperatingSystems"` + + SupportsHotAdd *bool `xml:"supportsHotAdd"` + SupportsHotPlugPCI *bool `xml:"supportsHotPlugPCI"` + SupportsNestedHV *bool `xml:"supportsNestedHV"` +} + +// HardDiskAdapter describes a hard disk controller type +type HardDiskAdapter struct { + Id string `xml:"id,attr"` + LegacyId int `xml:"legacyId,attr"` + Name string `xml:"name,attr"` + MaximumDiskSizeGb int `xml:"maximumDiskSizeGb,attr"` + + BusNumberRanges struct { + Begin int `xml:"begin,attr"` + End int `xml:"end,attr"` + } `xml:"BusNumberRanges>Range"` + UnitNumberRanges struct { + Begin int `xml:"begin,attr"` + End int `xml:"end,attr"` + } `xml:"UnitNumberRanges>Range"` + + ReservedBusUnitNumber struct { + BusNumber int `xml:"busNumber,attr"` + UnitNumber int `xml:"unitNumber,attr"` + } `xml:"ReservedBusUnitNumber"` +} + +// SupportedOperatingSystemsInfoType describes what operating system families a hardware version supports +type SupportedOperatingSystemsInfoType struct { + Link *Link + OperatingSystemFamilyInfo []*OperatingSystemFamilyInfoType `xml:"OperatingSystemFamilyInfo"` +} + +// OperatingSystemFamilyInfoType describes operating systems of a given OS family +type OperatingSystemFamilyInfoType struct { + Name string `xml:"Name"` + OperatingSystemFamilyId *int `xml:"OperatingSystemFamilyId"` + OperatingSystems []*OperatingSystemInfoType `xml:"OperatingSystem"` +} + +// OperatingSystemInfoType describes a operating system +type OperatingSystemInfoType struct { + OperatingSystemId *int `xml:"OperatingSystemId,omitempty"` + DefaultHardDiskAdapterType string `xml:"DefaultHardDiskAdapterType"` + SupportedHardDiskAdapter []struct { + Ref string `xml:"ref,attr"` + } `xml:"SupportedHardDiskAdapter,omitempty"` + MinimumHardDiskSizeGigabytes *int `xml:"MinimumHardDiskSizeGigabytes"` + MinimumMemoryMegabytes *int `xml:"MinimumMemoryMegabytes"` + Name string `xml:"Name"` + InternalName string `xml:"InternalName"` + Supported *bool `xml:"Supported"` + SupportLevel string `xml:"SupportLevel"` + X64 *bool `xml:"x64"` + MaximumCpuCount *int `xml:"MaximumCpuCount"` + MaximumCoresPerSocket *int `xml:"MaximumCoresPerSocket"` + MaximumSocketCount *int `xml:"MaximumSocketCount"` + MinimumHardwareVersion *int `xml:"MinimumHardwareVersion"` + PersonalizationEnabled *bool `xml:"PersonalizationEnabled"` + PersonalizationAuto *bool `xml:"PersonalizationAuto"` + SysprepPackagingSupported *bool `xml:"SysprepPackagingSupported"` + SupportsMemHotAdd *bool `xml:"SupportsMemHotAdd"` + CimOsId *int `xml:"cimOsId"` + CimVersion *int `xml:"CimVersion"` + SupportedForCreate *bool `xml:"SupportedForCreate"` + + RecommendedNIC []struct { + Name string `xml:"name,attr"` + Id *int `xml:"id,attr,omitempty"` + } `xml:"RecommendedNIC"` + + SupportedNICType []struct { + Name string `xml:"name,attr"` + Id *int `xml:"id,attr,omitempty"` + } `xml:"SupportedNICType"` + + RecommendedFirmware string `xml:"RecommendedFirmware"` + SupportedFirmware []string `xml:"SupportedFirmware"` + SupportsTPM *bool `xml:"SupportsTPM"` +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/vm_types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/vm_types.go index 9c33047f8..2bb1d40f9 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/vm_types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/vm_types.go @@ -65,6 +65,7 @@ type Vm struct { ProductSection *ProductSection `xml:"ProductSection,omitempty"` ComputePolicy *ComputePolicy `xml:"ComputePolicy,omitempty"` // accessible only from version API 33.0 Media *Reference `xml:"Media,omitempty"` // Reference to the media object to insert in a new VM. + BootOptions *BootOptions `xml:"BootOptions,omitempty"` // Accessible only from API version 37.1+ } type RuntimeInfoSection struct { @@ -82,6 +83,7 @@ type VmSpecSection struct { Modified *bool `xml:"Modified,attr,omitempty"` Info string `xml:"ovf:Info"` OsType string `xml:"OsType,omitempty"` // The type of the OS. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. + Firmware string `xml:"Firmware,omitempty"` // Available since API 37.1. VM's Firmware, can be either 'bios' or 'efi'. NumCpus *int `xml:"NumCpus,omitempty"` // Number of CPUs. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. NumCoresPerSocket *int `xml:"NumCoresPerSocket,omitempty"` // Number of cores among which to distribute CPUs in this virtual machine. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. CpuResourceMhz *CpuResourceMhz `xml:"CpuResourceMhz,omitempty"` // CPU compute resources. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. @@ -94,6 +96,16 @@ type VmSpecSection struct { TimeSyncWithHost *bool `xml:"TimeSyncWithHost,omitempty"` // Synchronize the VM's time with the host. } +// BootOptions allows to specify boot options of a VM +type BootOptions struct { + BootDelay *int `xml:"BootDelay,omitempty"` // Delay between power-on and boot of the VM + EnterBiosSetup *bool `xml:"EnterBIOSSetup,omitempty"` // Set to false on the next boot + BootRetryEnabled *bool `xml:"BootRetryEnabled,omitempty"` // Available since API 37.1 + BootRetryDelay *int `xml:"BootRetryDelay,omitempty"` // Available since API 37.1. Doesn't have an effect if BootRetryEnabled is set to false + EfiSecureBootEnabled *bool `xml:"EfiSecureBootEnabled,omitempty"` // Available since API 37.1 + NetworkBootProtocol string `xml:"NetworkBootProtocol,omitempty"` // Available since API 37.1 +} + // RecomposeVAppParamsForEmptyVm represents a vApp structure which allows to create VM. type RecomposeVAppParamsForEmptyVm struct { XMLName xml.Name `xml:"RecomposeVAppParams"` @@ -113,7 +125,8 @@ type CreateItem struct { VmSpecSection *VmSpecSection `xml:"VmSpecSection,omitempty"` StorageProfile *Reference `xml:"StorageProfile,omitempty"` ComputePolicy *ComputePolicy `xml:"ComputePolicy,omitempty"` // accessible only from version API 33.0 - BootImage *Media `xml:"Media,omitempty"` // boot image as vApp template. Href, Id and name needed. + BootOptions *BootOptions `xml:"BootOptions,omitempty"` + BootImage *Media `xml:"Media,omitempty"` // boot image as vApp template. Href, Id and name needed. } // ComputePolicy represents structure to manage VM compute polices, part of RecomposeVAppParams structure. diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/util/logging.go b/vendor/github.com/vmware/go-vcloud-director/v2/util/logging.go index e660e37de..819740d30 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/util/logging.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/util/logging.go @@ -41,6 +41,7 @@ const ( envLogSkipHttpReq = "GOVCD_LOG_SKIP_HTTP_REQ" // Name of the environment variable that enables logging of HTTP responses + // #nosec G101 -- Not a credential envLogSkipHttpResp = "GOVCD_LOG_SKIP_HTTP_RESP" // Name of the environment variable with a custom list of of responses to skip from logging @@ -203,6 +204,10 @@ func hideSensitive(in string, onScreen bool) string { re9 := regexp.MustCompile(`("refresh_token":\s*)"[^"]*`) out = re9.ReplaceAllString(out, `${1}*******`) + // API Token inside CSE JSON payloads + re10 := regexp.MustCompile(`("apiToken":\s*)"[^"]*`) + out = re10.ReplaceAllString(out, `${1}*******`) + return out } @@ -211,6 +216,12 @@ func isBinary(data string, req *http.Request) bool { reContentRange := regexp.MustCompile(`(?i)content-range`) reMultipart := regexp.MustCompile(`(?i)multipart/form`) reMediaXml := regexp.MustCompile(`(?i)media+xml;`) + // Skip data transferred for vApp template or catalog item upload + if strings.Contains(req.URL.String(), "/transfer/") && + (strings.HasSuffix(req.URL.String(), ".vmdk") || strings.HasSuffix(req.URL.String(), "/file")) && + (req.Method == http.MethodPut || req.Method == http.MethodPost) { + return true + } uiPlugin := regexp.MustCompile(`manifest\.json|bundle\.js`) for key, value := range req.Header { if reContentRange.MatchString(key) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 47ecf27fc..b6fbb5913 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -268,14 +268,14 @@ github.com/spf13/cast # github.com/spf13/pflag v1.0.5 ## explicit; go 1.12 github.com/spf13/pflag -# github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240422223843-6dd634aca882 +# github.com/vmware/cloud-provider-for-cloud-director v0.0.0-20240426203125-2d6e9efaf23d ## explicit; go 1.21 github.com/vmware/cloud-provider-for-cloud-director/pkg/testingsdk github.com/vmware/cloud-provider-for-cloud-director/pkg/util github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdsdk github.com/vmware/cloud-provider-for-cloud-director/pkg/vcdswaggerclient_37_2 -# github.com/vmware/go-vcloud-director/v2 v2.21.0 -## explicit; go 1.19 +# github.com/vmware/go-vcloud-director/v2 v2.24.0 +## explicit; go 1.21 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util