Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to the Runtime Defined Entities (RDE) framework with RDE types (2/3) #545

Merged
merged 43 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/v2.20.0/545-features.md
Original file line number Diff line number Diff line change
@@ -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]
194 changes: 194 additions & 0 deletions govcd/defined_entity.go
Original file line number Diff line number Diff line change
@@ -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
}
200 changes: 200 additions & 0 deletions govcd/defined_entity_test.go
Original file line number Diff line number Diff line change
@@ -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)
Didainius marked this conversation as resolved.
Show resolved Hide resolved

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
}
Loading