diff --git a/.changes/v2.20.0/545-features.md b/.changes/v2.20.0/545-features.md new file mode 100644 index 000000000..32de437d1 --- /dev/null +++ b/.changes/v2.20.0/545-features.md @@ -0,0 +1,3 @@ +* Added support for Runtime Defined Entity Types with client methods `VCDClient.CreateRdeType`, `VCDClient.GetAllRdeTypes`, + `VCDClient.GetRdeType`, `VCDClient.GetRdeTypeById` and methods to manipulate them `DefinedEntityType.Update`, + `DefinedEntityType.Delete` [GH-545] diff --git a/govcd/defined_entity.go b/govcd/defined_entity.go new file mode 100644 index 000000000..57eeb656f --- /dev/null +++ b/govcd/defined_entity.go @@ -0,0 +1,194 @@ +/* + * 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" +) + +// DefinedEntityType is a type for handling Runtime Defined Entity (RDE) Type definitions. +// Note. Running a few of these operations in parallel may corrupt database in VCD (at least <= 10.4.2) +type DefinedEntityType struct { + DefinedEntityType *types.DefinedEntityType + client *Client +} + +// 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 + } + + return result, nil +} + +// 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 + } + + // 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, + } + } + + return returnRDEs, nil +} + +// 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) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("vendor==%s;nss==%s;version==%s", vendor, nss, version)) + rdeTypes, err := vcdClient.GetAllRdeTypes(queryParameters) + if err != nil { + return nil, err + } + + if len(rdeTypes) == 0 { + return nil, fmt.Errorf("%s could not find the Runtime Defined Entity Type with vendor %s, nss %s and version %s", ErrorEntityNotFound, vendor, nss, version) + } + + if len(rdeTypes) > 1 { + return nil, fmt.Errorf("found more than 1 Runtime Defined Entity Type with vendor %s, nss %s and version %s", vendor, nss, version) + } + + return rdeTypes[0], nil +} + +// 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 + } + + 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 +} + +// 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") + } + + if rdeTypeToUpdate.ID != "" && rdeTypeToUpdate.ID != rdeType.DefinedEntityType.ID { + return fmt.Errorf("ID of the receiver Runtime Defined Entity and the input ID don't match") + } + + // Name and schema are mandatory, even when we don't want to update them, so we populate them in this situation to avoid errors + // and make this method more user friendly. + if rdeTypeToUpdate.Name == "" { + rdeTypeToUpdate.Name = rdeType.DefinedEntityType.Name + } + if rdeTypeToUpdate.Schema == nil || len(rdeTypeToUpdate.Schema) == 0 { + rdeTypeToUpdate.Schema = rdeType.DefinedEntityType.Schema + } + + 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 + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, rdeTypeToUpdate, rdeType.DefinedEntityType, nil) + if err != nil { + return err + } + + return nil +} + +// 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 + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + rdeType.DefinedEntityType = &types.DefinedEntityType{} + return nil +} diff --git a/govcd/defined_entity_test.go b/govcd/defined_entity_test.go new file mode 100644 index 000000000..baafba387 --- /dev/null +++ b/govcd/defined_entity_test.go @@ -0,0 +1,200 @@ +//go:build functional || openapi || rde || ALL +// +build functional openapi rde ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "io" + "os" + "path/filepath" + "strings" +) + +// Test_RdeType tests the CRUD operations for the RDE Type with both System administrator and a tenant user. +func (vcd *TestVCD) Test_RdeType(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes) + if len(vcd.config.Tenants) == 0 { + check.Skip("skipping as there is no configured tenant users") + } + + // Creates the clients for the System admin and the Tenant user + systemAdministratorClient := vcd.client + tenantUserClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := tenantUserClient.Authenticate(vcd.config.Tenants[0].User, vcd.config.Tenants[0].Password, vcd.config.Tenants[0].SysOrg) + check.Assert(err, IsNil) + + unmarshaledRdeTypeSchema, err := loadRdeTypeSchemaFromTestResources() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(unmarshaledRdeTypeSchema) > 0) + + // First, it checks how many exist already, as VCD contains some pre-defined ones. + allRdeTypesBySystemAdmin, err := systemAdministratorClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + alreadyPresentRdes := len(allRdeTypesBySystemAdmin) + + // For the tenant, it returns 0 RDE Types, but no error. + allRdeTypesByTenant, err := tenantUserClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + check.Assert(len(allRdeTypesByTenant), Equals, 0) + + // Then we create a new RDE Type with System administrator. + // Can't put check.TestName() in nss due to a bug in VCD 10.4.1 that causes RDEs to fail on GET once created with special characters like "." + vendor := "vmware" + nss := strings.ReplaceAll(check.TestName()+"name", ".", "") + version := "1.2.3" + rdeTypeToCreate := &types.DefinedEntityType{ + Name: check.TestName(), + Nss: nss, + Version: version, + Description: "Description of " + check.TestName(), + Schema: unmarshaledRdeTypeSchema, + Vendor: vendor, + Interfaces: []string{"urn:vcloud:interface:vmware:k8s:1.0.0"}, + } + createdRdeType, err := systemAdministratorClient.CreateRdeType(rdeTypeToCreate) + check.Assert(err, IsNil) + check.Assert(createdRdeType, NotNil) + check.Assert(createdRdeType.DefinedEntityType.Name, Equals, rdeTypeToCreate.Name) + check.Assert(createdRdeType.DefinedEntityType.Nss, Equals, rdeTypeToCreate.Nss) + check.Assert(createdRdeType.DefinedEntityType.Version, Equals, rdeTypeToCreate.Version) + check.Assert(createdRdeType.DefinedEntityType.Schema, NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["type"], Equals, "object") + check.Assert(createdRdeType.DefinedEntityType.Schema["definitions"], NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["required"], NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["properties"], NotNil) + AddToCleanupListOpenApi(createdRdeType.DefinedEntityType.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes+createdRdeType.DefinedEntityType.ID) + + // Tenants can't create RDE Types + nilRdeType, err := tenantUserClient.CreateRdeType(&types.DefinedEntityType{ + Name: check.TestName(), + Nss: "notworking", + Version: "4.5.6", + Schema: unmarshaledRdeTypeSchema, + Vendor: "willfail", + }) + check.Assert(err, NotNil) + check.Assert(nilRdeType, IsNil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + + // Assign rights to the tenant user, so it can perform following operations. + // We don't need to clean the rights afterwards as deleting the RDE Type deletes the associated bundle + // with its rights. + role, err := systemAdministratorClient.Client.GetGlobalRoleByName("Organization Administrator") + check.Assert(err, IsNil) + check.Assert(role, NotNil) + + rightsBundleName := fmt.Sprintf("%s:%s Entitlement", vendor, nss) + rightsBundle, err := systemAdministratorClient.Client.GetRightsBundleByName(rightsBundleName) + check.Assert(err, IsNil) + check.Assert(rightsBundle, NotNil) + + err = rightsBundle.PublishAllTenants() + check.Assert(err, IsNil) + + rights, err := rightsBundle.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Not(Equals), 0) + + var rightsToAdd []types.OpenApiReference + for _, right := range rights { + if strings.Contains(right.Name, fmt.Sprintf("%s:%s", vendor, nss)) { + rightsToAdd = append(rightsToAdd, types.OpenApiReference{ + Name: right.Name, + ID: right.ID, + }) + } + } + check.Assert(rightsToAdd, NotNil) + check.Assert(len(rightsToAdd), Not(Equals), 0) + + err = role.AddRights(rightsToAdd) + check.Assert(err, IsNil) + + // As we created a new RDE Type, we check the new count is correct in both System admin and Tenant user + allRdeTypesBySystemAdmin, err = systemAdministratorClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + check.Assert(len(allRdeTypesBySystemAdmin), Equals, alreadyPresentRdes+1) + + // Count is 1 for tenant user as it can only retrieve the created type as per the assigned rights above. + allRdeTypesByTenant, err = tenantUserClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + check.Assert(len(allRdeTypesByTenant), Equals, 1) + + // Test the multiple ways of getting a RDE Types in both users. + obtainedRdeTypeBySysAdmin, err := systemAdministratorClient.GetRdeTypeById(createdRdeType.DefinedEntityType.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeBySysAdmin.DefinedEntityType, DeepEquals, *createdRdeType.DefinedEntityType) + + // The RDE Type retrieved by the tenant should be the same as the retrieved by Sysadmin + obtainedRdeTypeByTenant, err := tenantUserClient.GetRdeTypeById(createdRdeType.DefinedEntityType.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeByTenant.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + obtainedRdeTypeBySysAdmin, err = systemAdministratorClient.GetRdeType(createdRdeType.DefinedEntityType.Vendor, createdRdeType.DefinedEntityType.Nss, createdRdeType.DefinedEntityType.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeBySysAdmin.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + // The RDE Type retrieved by the tenant should be the same as the retrieved by Sysadmin + obtainedRdeTypeByTenant, err = tenantUserClient.GetRdeType(createdRdeType.DefinedEntityType.Vendor, createdRdeType.DefinedEntityType.Nss, createdRdeType.DefinedEntityType.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeByTenant.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + // We don't want to update the name nor the schema. It should populate them from the receiver object automatically + err = obtainedRdeTypeBySysAdmin.Update(types.DefinedEntityType{ + Description: rdeTypeToCreate.Description + "UpdatedByAdmin", + }) + check.Assert(err, IsNil) + check.Assert(obtainedRdeTypeBySysAdmin.DefinedEntityType.Description, Equals, rdeTypeToCreate.Description+"UpdatedByAdmin") + + // We delete it with Sysadmin + deletedId := createdRdeType.DefinedEntityType.ID + err = createdRdeType.Delete() + check.Assert(err, IsNil) + check.Assert(*createdRdeType.DefinedEntityType, DeepEquals, types.DefinedEntityType{}) + + _, err = systemAdministratorClient.GetRdeTypeById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// loadRdeTypeSchemaFromTestResources loads the RDE schema present in the test-resources folder and unmarshals it +// into a map. Returns an error if something fails along the way. +func loadRdeTypeSchemaFromTestResources() (map[string]interface{}, error) { + // Load the RDE type schema + rdeFilePath := "../test-resources/rde_type.json" + _, err := os.Stat(rdeFilePath) + if os.IsNotExist(err) { + return nil, fmt.Errorf("unable to find RDE type file '%s': %s", rdeFilePath, err) + } + + rdeFile, err := os.OpenFile(filepath.Clean(rdeFilePath), os.O_RDONLY, 0400) + if err != nil { + return nil, fmt.Errorf("unable to open RDE type file '%s': %s", rdeFilePath, err) + } + defer safeClose(rdeFile) + + rdeSchema, err := io.ReadAll(rdeFile) + if err != nil { + return nil, fmt.Errorf("error reading RDE type file %s: %s", rdeFilePath, err) + } + + var unmarshaledJson map[string]interface{} + err = json.Unmarshal(rdeSchema, &unmarshaledJson) + if err != nil { + return nil, fmt.Errorf("could not unmarshal RDE type file %s: %s", rdeFilePath, err) + } + + return unmarshaledJson, nil +} diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index aab551d44..a1936a7a0 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -55,6 +55,7 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtRouteAdvertisement: "34.0", // VCD 10.1+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes: "35.0", // VCD 10.2+ // NSX-T ALB (Advanced/AVI Load Balancer) support was introduced in 10.2 types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController: "35.0", // VCD 10.2+ @@ -124,6 +125,10 @@ var endpointElevatedApiVersions = map[string][]string{ //"36.0", // Introduced support "36.2", // 2 additional fields vappNetworkSegmentProfileTemplateRef and vdcNetworkSegmentProfileTemplateRef added }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes: { + //"35.0", // Introduced support + "37.1", // Added MaxImplicitRight property in DefinedEntityType + }, } // checkOpenApiEndpointCompatibility checks if VCD version (to which the client is connected) is sufficient to work with diff --git a/test-resources/rde_type.json b/test-resources/rde_type.json new file mode 100644 index 000000000..91775659e --- /dev/null +++ b/test-resources/rde_type.json @@ -0,0 +1,41 @@ +{ + "definitions": { + "foo": { + "description": "Foo definition", + "properties": { + "key": { + "description": "Key for foo", + "type": "string" + } + }, + "type": "object" + } + }, + "properties": { + "bar": { + "description": "Bar", + "type": "string" + }, + "foo": { + "$ref": "#/definitions/foo" + }, + "prop2": { + "properties": { + "subprop1": { + "type": "string" + }, + "subprop2": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "required": [ + "foo" + ], + "type": "object" +} diff --git a/types/v56/constants.go b/types/v56/constants.go index 1800738ed..d21826e07 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -390,6 +390,7 @@ const ( OpenApiEndpointEdgeBgpConfigPrefixLists = "edgeGateways/%s/routing/bgp/prefixLists/" // '%s' is NSX-T Edge Gateway ID OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID OpenApiEndpointRdeInterfaces = "interfaces/" + OpenApiEndpointRdeEntityTypes = "entityTypes/" // NSX-T ALB related endpoints diff --git a/types/v56/openapi.go b/types/v56/openapi.go index 08ac450be..e904b4725 100644 --- a/types/v56/openapi.go +++ b/types/v56/openapi.go @@ -436,3 +436,20 @@ type DefinedInterface struct { Vendor string `json:"vendor,omitempty"` // The vendor name IsReadOnly bool `json:"readonly,omitempty"` // True if the entity type cannot be modified } + +// DefinedEntityType describes what a Defined Entity Type should look like. +type DefinedEntityType struct { + ID string `json:"id,omitempty"` // The id of the defined entity type in URN format + Name string `json:"name,omitempty"` // The name of the defined entity type + Nss string `json:"nss,omitempty"` // A unique namespace specific string. The combination of nss and version must be unique + Version string `json:"version,omitempty"` // The version of the defined entity. The combination of nss and version must be unique. The version string must follow semantic versioning rules + Description string `json:"description,omitempty"` // Description of the defined entity + ExternalId string `json:"externalId,omitempty"` // An external entity’s id that this definition may apply to + Hooks map[string]string `json:"hooks,omitempty"` // A mapping defining which behaviors should be invoked upon specific lifecycle events, like PostCreate, PostUpdate, PreDelete. For example: "hooks": { "PostCreate": "urn:vcloud:behavior-interface:postCreateHook:vendorA:containerCluster:1.0.0" }. Added in 36.0 + InheritedVersion string `json:"inheritedVersion,omitempty"` // To be used when creating a new version of a defined entity type. Specifies the version of the type that will be the template for the authorization configuration of the new version. The Type ACLs and the access requirements of the Type Behaviors of the new version will be copied from those of the inherited version. If the value of this property is ‘0’, then the new type version will not inherit another version and will have the default authorization settings, just like the first version of a new type. Added in 36.0 + Interfaces []string `json:"interfaces,omitempty"` // List of interface IDs that this defined entity type is referenced by + MaxImplicitRight string `json:"maxImplicitRight,omitempty"` // The maximum Type Right level that will be implied from the user’s Type ACLs if this field is defined. For example, “maxImplicitRight”: “urn:vcloud:accessLevel:ReadWrite” would mean that a user with RO , RW, and FC ACLs to the Type would implicitly get the “Read: ” and “Write: ” rights, but not the “Full Control: ” right. The valid values are “urn:vcloud:accessLevel:ReadOnly”, “urn:vcloud:accessLevel:ReadWrite”, “urn:vcloud:accessLevel:FullControl” + IsReadOnly bool `json:"readonly,omitempty"` // `true` if the entity type cannot be modified + Schema map[string]interface{} `json:"schema,omitempty"` // The JSON-Schema valid definition of the defined entity type. If no JSON Schema version is specified, version 4 will be assumed + Vendor string `json:"vendor,omitempty"` // The vendor name +}