diff --git a/.changes/v2.24.0/665-features.md b/.changes/v2.24.0/665-features.md new file mode 100644 index 000000000..4ae01b2f2 --- /dev/null +++ b/.changes/v2.24.0/665-features.md @@ -0,0 +1,6 @@ +* Added types `SolutionLandingZone` and `types.SolutionLandingZone` for Solution Add-on Landing Zone configuration [GH-665] +* Added method `DefinedEntity.Refresh` to reload RDE state [GH-665] +* Added `VDCClient` methods `CreateSolutionLandingZone`, `GetAllSolutionLandingZones`, + `GetExactlyOneSolutionLandingZone`, `GetSolutionLandingZoneById` for handling Solution Landing Zones [GH-665] +* Added `SolutionLandingZone` methods `Refresh`, `RdeId`, `Update`, + `Delete` to help handling of Solution Landing Zones [GH-665] diff --git a/go.mod b/go.mod index f78eff574..43d32c621 100644 --- a/go.mod +++ b/go.mod @@ -22,5 +22,5 @@ require ( replace ( gopkg.in/check.v1 => github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c - gopkg.in/yaml.v2 => github.com/go-yaml/yaml/v2 v2.2.2 + gopkg.in/yaml.v2 => github.com/go-yaml/yaml/v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 27859793b..ec2205cec 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c h1:3LdnoQiW6yLkxRIwSU3pbYp3zqW1daDgoOcOD09OzJs= github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -github.com/go-yaml/yaml/v2 v2.2.2 h1:uw2m9KuKRscWGAkuyoBGQcZSdibhmuXKSJ3+9Tj3zXc= -github.com/go-yaml/yaml/v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +github.com/go-yaml/yaml/v2 v2.4.0 h1:FNqNkD8zxfgoQ6pSknwk+CnijAT6ijXMqcUg7FXN3LU= +github.com/go-yaml/yaml/v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index ab4e492e6..040f52734 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -199,6 +199,7 @@ type TestConfig struct { VdcGroupEdgeGateway string `yaml:"vdcGroupEdgeGateway"` NsxtEdgeCluster string `yaml:"nsxtEdgeCluster"` RoutedNetwork string `yaml:"routedNetwork"` + IsolatedNetwork string `yaml:"isolatedNetwork"` NsxtAlbControllerUrl string `yaml:"nsxtAlbControllerUrl"` NsxtAlbControllerUser string `yaml:"nsxtAlbControllerUser"` NsxtAlbControllerPassword string `yaml:"nsxtAlbControllerPassword"` diff --git a/govcd/defined_entity.go b/govcd/defined_entity.go index 2c8d74fa0..3870d7069 100644 --- a/govcd/defined_entity.go +++ b/govcd/defined_entity.go @@ -7,9 +7,10 @@ package govcd import ( "encoding/json" "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" ) const ( @@ -482,6 +483,19 @@ func (rde *DefinedEntity) Resolve() error { return nil } +// Refresh reloads RDE +func (rde *DefinedEntity) Refresh() error { + client := rde.client + + refreshedRde, err := getRdeById(client, rde.DefinedEntity.ID) + if err != nil { + return fmt.Errorf("error refreshing RDE: %s", err) + } + rde.DefinedEntity = refreshedRde.DefinedEntity + + return nil +} + // Update updates the receiver Runtime Defined Entity with the values given by the input. This method is useful // if rde.Resolve() failed and a JSON entity change is needed. // Updating a RDE populates the ETag field in the receiver object. diff --git a/govcd/generic_functions.go b/govcd/generic_functions.go index da681dcf7..db50d74c5 100644 --- a/govcd/generic_functions.go +++ b/govcd/generic_functions.go @@ -1,6 +1,7 @@ package govcd import ( + "encoding/json" "fmt" "reflect" ) @@ -96,3 +97,34 @@ func localFilterOneOrError[E any](entityLabel string, entities []*E, fieldName, return oneOrError(fieldName, expectedFieldValue, filteredValues) } + +// convertAnyToRdeEntity unmarshals any entity to map[string]interface{} +func convertAnyToRdeEntity[E any](entityCfg *E) (map[string]interface{}, error) { + jsonText, err := json.Marshal(entityCfg) + if err != nil { + return nil, fmt.Errorf("error marshalling configuration :%s", err) + } + + var unmarshalledRdeEntityJson map[string]interface{} + err = json.Unmarshal(jsonText, &unmarshalledRdeEntityJson) + if err != nil { + return nil, fmt.Errorf("error unmarshalling configuration :%s", err) + } + + return unmarshalledRdeEntityJson, nil +} + +func convertRdeEntityToAny[E any](content map[string]interface{}) (*E, error) { + jsonText2, err := json.Marshal(content) + if err != nil { + return nil, fmt.Errorf("error converting entity to type: %s", err) + } + + result := new(E) + err = json.Unmarshal(jsonText2, result) + if err != nil { + return nil, fmt.Errorf("error converting entity to type: %s", err) + } + + return result, nil +} diff --git a/govcd/landing_zone.go b/govcd/landing_zone.go new file mode 100644 index 000000000..8ebf07526 --- /dev/null +++ b/govcd/landing_zone.go @@ -0,0 +1,214 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// slzRdeType sets Runtime Defined Entity Type to be used across multiple calls +var slzRdeType = [3]string{"vmware", "solutions_organization", "1.0.0"} + +// SolutionLandingZone controls VCD Solution Add-On Landing Zone. It does so by wrapping RDE for +// entity types vmware:solutions_organization:1.0.0. +// +// Up to VCD 10.5.1.1 ,there can only be one single RDE instance for landing zone. +type SolutionLandingZone struct { + // SolutionLandingZoneType defines internal content of RDE (`types.DefinedEntity.State`) + SolutionLandingZoneType *types.SolutionLandingZoneType + // DefinedEntity contains parent defined entity that contains SolutionLandingZoneType in + // "Entity" field + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// CreateSolutionLandingZone configures VCD Solution Add-On Landing Zone. It does so by performing +// the following steps: +// +// 1. Creates Solution Landing Zone RDE based on type urn:vcloud:type:vmware:solutions_organization:1.0.0 +// 2. Resolves the RDE +func (vcdClient *VCDClient) CreateSolutionLandingZone(slzCfg *types.SolutionLandingZoneType) (*SolutionLandingZone, error) { + // 1. Check that RDE type exists + rdeType, err := vcdClient.GetRdeType(slzRdeType[0], slzRdeType[1], slzRdeType[2]) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE Type for Solution Landing zone: %s", err) + } + + // 2. Convert more precise structure to fit DefinedEntity.DefinedEntity.Entity + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(slzCfg) + if err != nil { + return nil, err + } + + // 3. Construct payload + entityCfg := &types.DefinedEntity{ + EntityType: "urn:vcloud:type:" + strings.Join(slzRdeType[:], ":"), + Name: "Solutions Organization", + State: addrOf("PRE_CREATED"), + // Processed solution landing zone + Entity: unmarshalledRdeEntityJson, + } + + // 4. Create RDE + createdRdeEntity, err := rdeType.CreateRde(*entityCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating RDE entity: %s", err) + } + + // 5. Resolve RDE + err = createdRdeEntity.Resolve() + if err != nil { + return nil, fmt.Errorf("error resolving Solutions add-on after creating: %s", err) + } + + // 6. Reload RDE + err = createdRdeEntity.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing RDE after resolving: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](createdRdeEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + returnType := SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: vcdClient, + DefinedEntity: createdRdeEntity, + } + + return &returnType, nil +} + +// GetAllSolutionLandingZones retrieves all Solution Landing Zones +// +// Note: Up to VCD 10.5.1.1 there can be only a single RDE entry (one SLZ per VCD) +func (vcdClient *VCDClient) GetAllSolutionLandingZones(queryParameters url.Values) ([]*SolutionLandingZone, error) { + allSlzs, err := vcdClient.GetAllRdes(slzRdeType[0], slzRdeType[1], slzRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all SLZs: %s", err) + } + + results := make([]*SolutionLandingZone, len(allSlzs)) + for slzRdeIndex, slzRde := range allSlzs { + + slz, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slzRde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to SLZ: %s", err) + } + + results[slzRdeIndex] = &SolutionLandingZone{ + vcdClient: vcdClient, + DefinedEntity: slzRde, + SolutionLandingZoneType: slz, + } + } + + return results, nil +} + +// GetExactlyOneSolutionLandingZone will get single Solution Landing Zone RDE or fail. +// There can be only one Solution Landing Zone in VCD, but because it is backed by RDE - it can +// occur that due to some error there is more than one RDE Entity +func (vcdClient *VCDClient) GetExactlyOneSolutionLandingZone() (*SolutionLandingZone, error) { + allSlzs, err := vcdClient.GetAllSolutionLandingZones(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all Solution Landing Zones: %s", err) + } + + return oneOrError("rde", strings.Join(slzRdeType[:], ":"), allSlzs) +} + +// GetSolutionLandingZoneById retrieves Solution Landing Zone by ID +// +// Note: defined entity ID must be used that can be accessed either by `SolutionLandingZone.Id()` +// method or directly in `SolutionLandingZone.DefinedEntity.DefinedEntity.ID` field +func (vcdClient *VCDClient) GetSolutionLandingZoneById(id string) (*SolutionLandingZone, error) { + if id == "" { + return nil, fmt.Errorf("id must be specified") + } + rde, err := getRdeById(&vcdClient.Client, id) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE by ID: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](rde.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := &SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: vcdClient, + DefinedEntity: rde, + } + + return packages, nil +} + +// Refresh reloads parent RDE data +func (slz *SolutionLandingZone) Refresh() error { + err := slz.DefinedEntity.Refresh() + if err != nil { + return err + } + + // Repackage created RDE "Entity" to more exact type + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slz.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return err + } + + slz.SolutionLandingZoneType = result + + return nil +} + +// RdeId is a shorthand to retrieve ID of parent runtime defined entity +func (slz *SolutionLandingZone) RdeId() string { + if slz == nil || slz.DefinedEntity == nil || slz.DefinedEntity.DefinedEntity == nil { + return "" + } + + return slz.DefinedEntity.DefinedEntity.ID +} + +// Update Solution Landing Zone +func (slz *SolutionLandingZone) Update(slzCfg *types.SolutionLandingZoneType) (*SolutionLandingZone, error) { + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(slzCfg) + if err != nil { + return nil, err + } + + slz.DefinedEntity.DefinedEntity.Entity = unmarshalledRdeEntityJson + + err = slz.DefinedEntity.Update(*slz.DefinedEntity.DefinedEntity) + if err != nil { + return nil, err + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slz.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: slz.vcdClient, + DefinedEntity: slz.DefinedEntity, + } + + return &packages, nil +} + +// Delete removes the RDE that defines Solution Landing Zone +func (slz *SolutionLandingZone) Delete() error { + return slz.DefinedEntity.Delete() +} diff --git a/govcd/landing_zone_test.go b/govcd/landing_zone_test.go new file mode 100644 index 000000000..0b092e564 --- /dev/null +++ b/govcd/landing_zone_test.go @@ -0,0 +1,134 @@ +//go:build slz || functional || ALL + +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_CreateLandingZone(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Solution Landing Zones are supported in VCD 10.4.1+") + } + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + + adminVdc, err := adminOrg.GetAdminVDCById(vcd.nsxtVdc.Vdc.ID, false) + check.Assert(err, IsNil) + + orgNetwork, err := vcd.nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.RoutedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork, NotNil) + + computePolicy, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + check.Assert(computePolicy, NotNil) + + storageProfileRef, err := adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(storageProfileRef, NotNil) + + catalog, err := adminOrg.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + + slzCfg := &types.SolutionLandingZoneType{ + Name: adminOrg.AdminOrg.Name, + ID: adminOrg.AdminOrg.ID, + Vdcs: []types.SolutionLandingZoneVdc{ + { + ID: adminVdc.AdminVdc.ID, + Name: adminVdc.AdminVdc.Name, + Capabilities: []string{}, + Networks: []types.SolutionLandingZoneVdcChild{ + { + ID: orgNetwork.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork.OpenApiOrgVdcNetwork.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + ComputePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: computePolicy[0].VdcComputePolicyV2.ID, + Name: computePolicy[0].VdcComputePolicyV2.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + StoragePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: storageProfileRef.ID, + Name: storageProfileRef.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + }, + }, + Catalogs: []types.SolutionLandingZoneCatalog{ + { + ID: catalog.Catalog.ID, + Name: catalog.Catalog.Name, + Capabilities: []string{}, + }, + }, + } + + slz, err := vcd.client.CreateSolutionLandingZone(slzCfg) + check.Assert(err, IsNil) + check.Assert(slz, NotNil) + + AddToCleanupListOpenApi(slz.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+slz.DefinedEntity.DefinedEntity.ID) + + err = slz.Refresh() + check.Assert(err, IsNil) + + // Get all + allEntries, err := vcd.client.GetAllSolutionLandingZones(nil) + check.Assert(err, IsNil) + + check.Assert(len(allEntries), Equals, 1) + check.Assert(allEntries[0].RdeId(), Equals, slz.RdeId()) + + // Get by ID + slzById, err := vcd.client.GetSolutionLandingZoneById(slz.RdeId()) + check.Assert(err, IsNil) + check.Assert(slzById.RdeId(), Equals, slz.RdeId()) + + // Get exactly one + slzSingle, err := vcd.client.GetExactlyOneSolutionLandingZone() + check.Assert(err, IsNil) + check.Assert(slzSingle.RdeId(), Equals, slz.RdeId()) + + // Update + // Lookup one more Org network and add it + orgNetwork2, err := vcd.nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.IsolatedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork2, NotNil) + + slzCfg.Vdcs[0].Networks = append(slzCfg.Vdcs[0].Networks, types.SolutionLandingZoneVdcChild{ + ID: orgNetwork2.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork2.OpenApiOrgVdcNetwork.Name, + IsDefault: false, + Capabilities: []string{}, + }) + + updatedSlz, err := slz.Update(slzCfg) + check.Assert(err, IsNil) + check.Assert(len(updatedSlz.SolutionLandingZoneType.Vdcs[0].Networks), Equals, 2) + + err = slz.Delete() + check.Assert(err, IsNil) + + // Check that no entry exists + slzByIdErr, err := vcd.client.GetSolutionLandingZoneById(slz.RdeId()) + check.Assert(err, NotNil) + check.Assert(slzByIdErr, IsNil) +} diff --git a/types/v56/slz.go b/types/v56/slz.go new file mode 100644 index 000000000..406936ab2 --- /dev/null +++ b/types/v56/slz.go @@ -0,0 +1,35 @@ +package types + +// SolutionLandingZoneType defines the configuration of Solution Landing Zone. +// It uses RDE so this body must be inserted into `types.DefinedEntity.State` field +type SolutionLandingZoneType struct { + // ID is the Org ID that the Solution Landing Zone is configured for + ID string `json:"id"` + // Name is the Org name that the Solution Landing Zone is configured for + Name string `json:"name,omitempty"` + Catalogs []SolutionLandingZoneCatalog `json:"catalogs"` + Vdcs []SolutionLandingZoneVdc `json:"vdcs"` +} + +type SolutionLandingZoneCatalog struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Capabilities []string `json:"capabilities"` +} + +type SolutionLandingZoneVdc struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Capabilities []string `json:"capabilities"` + IsDefault bool `json:"isDefault"` + Networks []SolutionLandingZoneVdcChild `json:"networks"` + StoragePolicies []SolutionLandingZoneVdcChild `json:"storagePolicies"` + ComputePolicies []SolutionLandingZoneVdcChild `json:"computePolicies"` +} + +type SolutionLandingZoneVdcChild struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + IsDefault bool `json:"isDefault"` + Capabilities []string `json:"capabilities"` +}