diff --git a/.changes/v2.20.0/544-features.md b/.changes/v2.20.0/544-features.md new file mode 100644 index 000000000..43dc90fe1 --- /dev/null +++ b/.changes/v2.20.0/544-features.md @@ -0,0 +1,5 @@ +* Added support for Runtime Defined Entity instances with methods `DefinedEntityType.GetAllRdes`, `DefinedEntityType.GetRdeByName`, + `DefinedEntityType.GetRdeById`, `DefinedEntityType.CreateRde` and methods to manipulate them `DefinedEntity.Resolve`, + `DefinedEntity.Update`, `DefinedEntity.Delete` [GH-544] +* Add generic `Client` methods `OpenApiPostItemAndGetHeaders` and `OpenApiGetItemAndHeaders` to be able to retrieve the + response headers when performing a POST or GET operation to an OpenAPI endpoint [GH-544] diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 7bd0fef9c..e15945241 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -800,7 +800,7 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { // so we need to amend them isBuggyRdeError := strings.Contains(entity.OpenApiEndpoint, types.OpenApiEndpointRdeInterfaces) if isBuggyRdeError { - err = amendDefinedInterfaceError(&vcd.client.Client, err) + err = amendRdeApiError(&vcd.client.Client, err) } if ContainsNotFound(err) { diff --git a/govcd/defined_entity.go b/govcd/defined_entity.go index 57eeb656f..327b0f97e 100644 --- a/govcd/defined_entity.go +++ b/govcd/defined_entity.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" + "time" ) // DefinedEntityType is a type for handling Runtime Defined Entity (RDE) Type definitions. @@ -17,6 +18,13 @@ type DefinedEntityType struct { client *Client } +// DefinedEntity represents an instance of a Runtime Defined Entity (RDE) +type DefinedEntity struct { + DefinedEntity *types.DefinedEntity + Etag string // Populated by VCDClient.GetRdeById, DefinedEntityType.GetRdeById, DefinedEntity.Update + 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) { @@ -192,3 +200,298 @@ func (rdeType *DefinedEntityType) Delete() error { rdeType.DefinedEntityType = &types.DefinedEntityType{} return nil } + +// GetAllRdes gets all the RDE instances of the given vendor, nss and version. +func (vcdClient *VCDClient) GetAllRdes(vendor, nss, version string, queryParameters url.Values) ([]*DefinedEntity, error) { + return getAllRdes(&vcdClient.Client, vendor, nss, version, queryParameters) +} + +// GetAllRdes gets all the RDE instances of the receiver type. +func (rdeType *DefinedEntityType) GetAllRdes(queryParameters url.Values) ([]*DefinedEntity, error) { + return getAllRdes(rdeType.client, rdeType.DefinedEntityType.Vendor, rdeType.DefinedEntityType.Nss, rdeType.DefinedEntityType.Version, queryParameters) +} + +// 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, + } + } + + return returnRDEs, nil +} + +// GetRdesByName gets RDE instances with the given name that belongs to the receiver type. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func (rdeType *DefinedEntityType) GetRdesByName(name string) ([]*DefinedEntity, error) { + return getRdesByName(rdeType.client, rdeType.DefinedEntityType.Vendor, rdeType.DefinedEntityType.Nss, rdeType.DefinedEntityType.Version, name) +} + +// GetRdesByName gets RDE instances with the given name and the given vendor, nss and version. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func (vcdClient *VCDClient) GetRdesByName(vendor, nss, version, name string) ([]*DefinedEntity, error) { + return getRdesByName(&vcdClient.Client, vendor, nss, version, name) +} + +// getRdesByName gets RDE instances with the given name and the given vendor, nss and version. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func getRdesByName(client *Client, vendor, nss, version, name string) ([]*DefinedEntity, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("name==%s", name)) + rdeTypes, err := getAllRdes(client, vendor, nss, version, queryParameters) + if err != nil { + return nil, err + } + + if len(rdeTypes) == 0 { + return nil, fmt.Errorf("%s could not find the Runtime Defined Entity with name '%s'", ErrorEntityNotFound, name) + } + + return rdeTypes, nil +} + +// GetRdeById gets a Runtime Defined Entity by its ID. +// Getting a RDE by ID populates the ETag field in the returned object. +func (rdeType *DefinedEntityType) GetRdeById(id string) (*DefinedEntity, error) { + return getRdeById(rdeType.client, id) +} + +// GetRdeById gets a Runtime Defined Entity by its ID. +// Getting a RDE by ID populates the ETag field in the returned object. +func (vcdClient *VCDClient) GetRdeById(id string) (*DefinedEntity, error) { + return getRdeById(&vcdClient.Client, id) +} + +// 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 + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + 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 +} + +// CreateRde creates an entity of the type of the receiver Runtime Defined Entity (RDE) type. +// The input doesn't need to specify the type ID, as it gets it from the receiver RDE type. +// The input tenant context allows to create the RDE in a given org if the creator is a System admin. +// 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 (rdeType *DefinedEntityType) CreateRde(entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { + entity.EntityType = rdeType.DefinedEntityType.ID + 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) +} + +// 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) { + entity.EntityType = fmt.Sprintf("urn:vcloud:type:%s:%s:%s", vendor, nss, version) + err := createRde(&vcdClient.Client, entity, tenantContext) + if err != nil { + return nil, err + } + return pollPreCreatedRde(&vcdClient.Client, vendor, nss, version, entity.Name, 5) +} + +// CreateRde creates an entity of the type of the receiver Runtime Defined Entity (RDE) type. +// The input doesn't need to specify the type ID, as it gets it from the receiver RDE type. If it is specified anyway, +// 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 { + if entity.EntityType == "" { + return 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") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, entity.EntityType) + if err != nil { + return err + } + + _, err = client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, nil, entity, getTenantContextHeader(tenantContext)) + if err != nil { + return err + } + return 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 + } + } + } + time.Sleep(3 * time.Second) + } + return nil, fmt.Errorf("could not create RDE, failed during retrieval after creation: %s", err) +} + +// Resolve needs to be called after an RDE is successfully created. It makes the receiver RDE usable if the JSON entity +// is valid, reaching a state of RESOLVED. If it fails, the state will be RESOLUTION_ERROR, +// and it will need to Update the JSON entity. +// Resolving a RDE populates the ETag field in the receiver object. +func (rde *DefinedEntity) Resolve() error { + client := rde.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rde.DefinedEntity.ID)) + if err != nil { + return err + } + + headers, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, nil, rde.DefinedEntity, nil) + if err != nil { + return amendRdeApiError(client, err) + } + rde.Etag = headers.Get("Etag") + + 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. +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") + } + + // Name is mandatory, despite we don't want to update it, so we populate it in this situation to avoid errors + // and make this method more user friendly. + if rdeToUpdate.Name == "" { + rdeToUpdate.Name = rde.DefinedEntity.Name + } + + if rde.Etag == "" { + // We need to get an Etag to perform the update + retrievedRde, err := getRdeById(rde.client, rde.DefinedEntity.ID) + if err != nil { + return err + } + if retrievedRde.Etag == "" { + return fmt.Errorf("could not retrieve a valid Etag to perform an update to RDE %s", retrievedRde.DefinedEntity.ID) + } + 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) + } + + headers, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, nil, rdeToUpdate, rde.DefinedEntity, map[string]string{"If-Match": rde.Etag}) + if err != nil { + return err + } + rde.Etag = headers.Get("Etag") + + return nil +} + +// 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") + } + + 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) + } + + rde.DefinedEntity = &types.DefinedEntity{} + rde.Etag = "" + return nil +} diff --git a/govcd/defined_entity_test.go b/govcd/defined_entity_test.go index baafba387..c20e2d8b0 100644 --- a/govcd/defined_entity_test.go +++ b/govcd/defined_entity_test.go @@ -1,5 +1,4 @@ //go:build functional || openapi || rde || ALL -// +build functional openapi rde ALL /* * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -18,13 +17,20 @@ import ( "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) { +// Test_RdeAndRdeType tests the CRUD operations for the RDE Type with both System administrator and a tenant user. +func (vcd *TestVCD) Test_RdeAndRdeType(check *C) { if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes) + for _, endpoint := range []string{ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + } { + skipOpenApiEndpointTest(vcd, check, endpoint) + } + if len(vcd.config.Tenants) == 0 { check.Skip("skipping as there is no configured tenant users") } @@ -158,6 +164,9 @@ func (vcd *TestVCD) Test_RdeType(check *C) { check.Assert(err, IsNil) check.Assert(obtainedRdeTypeBySysAdmin.DefinedEntityType.Description, Equals, rdeTypeToCreate.Description+"UpdatedByAdmin") + testRdeCrudWithGivenType(check, obtainedRdeTypeBySysAdmin) + testRdeCrudAsTenant(check, obtainedRdeTypeByTenant.DefinedEntityType.Vendor, obtainedRdeTypeByTenant.DefinedEntityType.Nss, obtainedRdeTypeByTenant.DefinedEntityType.Version, vcd.client) + // We delete it with Sysadmin deletedId := createdRdeType.DefinedEntityType.ID err = createdRdeType.Delete() @@ -169,6 +178,156 @@ func (vcd *TestVCD) Test_RdeType(check *C) { check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) } +// testRdeCrudWithGivenType is a sub-section of Test_Rde that is focused on testing all RDE instances casuistics. +// This would be the viewpoint of a System admin as they can retrieve and manipulate RDE types. +func testRdeCrudWithGivenType(check *C, rdeType *DefinedEntityType) { + + // We are missing the mandatory field "foo" on purpose + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + } + }`) + + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := rdeType.CreateRde(types.DefinedEntity{ + Name: check.TestName(), + ExternalId: "123", + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + check.Assert(rde.DefinedEntity.Name, Equals, check.TestName()) + check.Assert(*rde.DefinedEntity.State, Equals, "PRE_CREATED") + + // If we don't resolve the RDE, we cannot delete it + err = rde.Delete() + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "RDE_ENTITY_NOT_RESOLVED")) + + // Resolution should fail as we missed to add a mandatory field + err = rde.Resolve() + eTag := rde.Etag + check.Assert(err, IsNil) + // The RDE can be automatically deleted now as rde.Resolve() was called successfully + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(eTag, Not(Equals), "") + + // We amend it + unmarshaledRdeEntityJson["foo"] = map[string]interface{}{"key": "stringValue5"} + err = rde.Update(types.DefinedEntity{ + Entity: unmarshaledRdeEntityJson, + }) + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + eTag = rde.Etag + + // This time it should resolve + err = rde.Resolve() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLVED") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + + // Delete the RDE instance now that it's resolved + deletedId := rde.DefinedEntity.ID + err = rde.Delete() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity, DeepEquals, types.DefinedEntity{}) + + // RDE should not exist anymore + _, err = rdeType.GetRdeById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// testRdeCrudAsTenant is a sub-section of Test_Rde that is focused on testing all RDE instances casuistics without specifying the +// RDE type. This would be the viewpoint of a tenant as they can't get RDE types. +func testRdeCrudAsTenant(check *C, vendor string, namespace string, version string, vcdClient *VCDClient) { + // We are missing the mandatory field "foo" on purpose + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + } + }`) + + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := vcdClient.CreateRde(vendor, namespace, version, types.DefinedEntity{ + Name: check.TestName(), + ExternalId: "123", + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + check.Assert(rde.DefinedEntity.Name, Equals, check.TestName()) + check.Assert(*rde.DefinedEntity.State, Equals, "PRE_CREATED") + + // If we don't resolve the RDE, we cannot delete it + err = rde.Delete() + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "RDE_ENTITY_NOT_RESOLVED")) + + // Resolution should fail as we missed to add a mandatory field + err = rde.Resolve() + eTag := rde.Etag + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(eTag, Not(Equals), "") + + // We amend it + unmarshaledRdeEntityJson["foo"] = map[string]interface{}{"key": "stringValue5"} + err = rde.Update(types.DefinedEntity{ + Entity: unmarshaledRdeEntityJson, + }) + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + eTag = rde.Etag + + // This time it should resolve + err = rde.Resolve() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLVED") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + + // The RDE can't be deleted until rde.Resolve() is called + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + + // Delete the RDE instance now that it's resolved + deletedId := rde.DefinedEntity.ID + err = rde.Delete() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity, DeepEquals, types.DefinedEntity{}) + check.Assert(rde.Etag, Equals, "") + + // RDE should not exist anymore + _, err = vcdClient.GetRdeById(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) { diff --git a/govcd/defined_interface.go b/govcd/defined_interface.go index 20670c1ed..4a522d516 100644 --- a/govcd/defined_interface.go +++ b/govcd/defined_interface.go @@ -63,7 +63,7 @@ func (vcdClient *VCDClient) GetAllDefinedInterfaces(queryParameters url.Values) typeResponses := []*types.DefinedInterface{{}} err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) if err != nil { - return nil, amendDefinedInterfaceError(&client, err) + return nil, amendRdeApiError(&client, err) } // Wrap all typeResponses into DefinedEntityType types with client @@ -120,7 +120,7 @@ func (vcdClient *VCDClient) GetDefinedInterfaceById(id string) (*DefinedInterfac err = client.OpenApiGetItem(apiVersion, urlRef, nil, result.DefinedInterface, nil) if err != nil { - return nil, amendDefinedInterfaceError(&client, err) + return nil, amendRdeApiError(&client, err) } return result, nil @@ -152,7 +152,7 @@ func (di *DefinedInterface) Update(definedInterface types.DefinedInterface) erro err = client.OpenApiPutItem(apiVersion, urlRef, nil, definedInterface, di.DefinedInterface, nil) if err != nil { - return amendDefinedInterfaceError(client, err) + return amendRdeApiError(client, err) } return nil @@ -180,16 +180,16 @@ func (di *DefinedInterface) Delete() error { err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) if err != nil { - return amendDefinedInterfaceError(client, err) + return amendRdeApiError(client, err) } di.DefinedInterface = &types.DefinedInterface{} return nil } -// amendDefinedInterfaceError fixes a wrong type of error returned by VCD API <= v36.0 on GET operations +// amendRdeApiError fixes a wrong type of error returned by VCD API <= v36.0 on GET operations // when the defined interface does not exist. -func amendDefinedInterfaceError(client *Client, err error) error { +func amendRdeApiError(client *Client, err error) error { if client.APIClientVersionIs("<= 36.0") && err != nil && strings.Contains(err.Error(), "does not exist") { return fmt.Errorf("%s: %s", ErrorEntityNotFound.Error(), err) } diff --git a/govcd/openapi.go b/govcd/openapi.go index add3082d6..77aa2c847 100644 --- a/govcd/openapi.go +++ b/govcd/openapi.go @@ -118,8 +118,18 @@ func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, que // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') // It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is // returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to -// differentiate when an objects was not found from any other error. +// differentiate when an object was not found from any other error. func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, params, outType, additionalHeader) + return err +} + +// OpenApiGetItemAndHeaders is a low level OpenAPI client function to perform GET request for any item and return all the headers. +// The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') +// It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is +// returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to +// differentiate when an object was not found from any other error. +func (client *Client) OpenApiGetItemAndHeaders(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -127,13 +137,13 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } req := client.newOpenApiRequest(apiVersion, params, http.MethodGet, urlRefCopy, nil, additionalHeader) resp, err := client.Http.Do(req) if err != nil { - return fmt.Errorf("error performing GET request to %s: %s", urlRefCopy.String(), err) + return nil, fmt.Errorf("error performing GET request to %s: %s", urlRefCopy.String(), err) } // Bypassing the regular path using function checkRespWithErrType and returning parsed error directly @@ -141,7 +151,7 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params if resp.StatusCode == http.StatusForbidden { err := ParseErr(types.BodyTypeJSON, resp, &types.OpenApiError{}) closeErr := resp.Body.Close() - return fmt.Errorf("%s: %s [body close error: %s]", ErrorEntityNotFound, err, closeErr) + return nil, fmt.Errorf("%s: %s [body close error: %s]", ErrorEntityNotFound, err, closeErr) } // resp is ignored below because it is the same as above @@ -149,19 +159,19 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params // Any other error occurred if err != nil { - return fmt.Errorf("error in HTTP GET request: %s", err) + return nil, fmt.Errorf("error in HTTP GET request: %s", err) } if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after GET: %s", err) + return nil, fmt.Errorf("error decoding JSON response after GET: %s", err) } err = resp.Body.Close() if err != nil { - return fmt.Errorf("error closing response body: %s", err) + return nil, fmt.Errorf("error closing response body: %s", err) } - return nil + return resp.Header, nil } // OpenApiPostItemSync is a low level OpenAPI client function to perform POST request for items that support synchronous @@ -212,6 +222,16 @@ func (client *Client) OpenApiPostItemSync(apiVersion string, urlRef *url.URL, pa // Note. Even though it may return error if the item does not support asynchronous request - the object may still be // created. OpenApiPostItem would handle both cases and always return created item. func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}) (Task, error) { + return client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, params, payload, nil) +} + +// OpenApiPostItemAsyncWithHeaders is a low level OpenAPI client function to perform POST request for items that support +// asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports asynchronous +// requests. It will return an error if item does not support asynchronous request (does not respond with HTTP 202). +// +// Note. Even though it may return error if the item does not support asynchronous request - the object may still be +// created. OpenApiPostItem would handle both cases and always return created item. +func (client *Client) OpenApiPostItemAsyncWithHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (Task, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -222,7 +242,7 @@ func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, p return Task{}, fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, nil) + resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { return Task{}, err } @@ -251,6 +271,14 @@ func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, p // asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is // synchronous - it will track task until it is finished and pick reference to marshal outType. func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) + return err +} + +// OpenApiPostItemAndGetHeaders is a low level OpenAPI client function to perform POST request for item supporting synchronous or +// asynchronous requests, that returns also the response headers. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is +// synchronous - it will track task until it is finished and pick reference to marshal outType. +func (client *Client) OpenApiPostItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -258,12 +286,12 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { - return err + return nil, err } // Handle two cases of API behaviour - synchronous (response status code is 200 or 201) and asynchronous (response status @@ -277,7 +305,7 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params task.Task.HREF = taskUrl err = task.WaitTaskCompletion() if err != nil { - return fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) + return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) } // Here we have to find the resource once more to return it populated. @@ -287,23 +315,23 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params newObjectUrl := urlParseRequestURI(urlRefCopy.String() + task.Task.Owner.ID) err = client.OpenApiGetItem(apiVersion, newObjectUrl, nil, outType, additionalHeader) if err != nil { - return fmt.Errorf("error retrieving item after creation: %s", err) + return nil, fmt.Errorf("error retrieving item after creation: %s", err) } // Synchronous task - new item body is returned in response of HTTP POST request case http.StatusCreated, http.StatusOK: util.Logger.Printf("[TRACE] Synchronous task detected (HTTP Status %d), marshalling outType '%s'", resp.StatusCode, reflect.TypeOf(outType)) if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after POST: %s", err) + return nil, fmt.Errorf("error decoding JSON response after POST: %s", err) } } err = resp.Body.Close() if err != nil { - return fmt.Errorf("error closing response body: %s", err) + return nil, fmt.Errorf("error closing response body: %s", err) } - return nil + return resp.Header, nil } // OpenApiPutItemSync is a low level OpenAPI client function to perform PUT request for items that support synchronous @@ -391,6 +419,14 @@ func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, pa // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) + return err +} + +// OpenApiPutItemAndGetHeaders is a low level OpenAPI client function to perform PUT request for any item and return the response headers. +// The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') +// It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. +func (client *Client) OpenApiPutItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -398,12 +434,12 @@ func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { - return err + return nil, err } // Handle two cases of API behaviour - synchronous (response status code is 201) and asynchronous (response status @@ -417,29 +453,29 @@ func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params task.Task.HREF = taskUrl err = task.WaitTaskCompletion() if err != nil { - return fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) + return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) } // Here we have to find the resource once more to return it populated. Provided params ir ignored for retrieval. err = client.OpenApiGetItem(apiVersion, urlRefCopy, nil, outType, additionalHeader) if err != nil { - return fmt.Errorf("error retrieving item after updating: %s", err) + return nil, fmt.Errorf("error retrieving item after updating: %s", err) } // Synchronous task - new item body is returned in response of HTTP PUT request case http.StatusOK: util.Logger.Printf("[TRACE] Synchronous task detected, marshalling outType '%s'", reflect.TypeOf(outType)) if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after PUT: %s", err) + return nil, fmt.Errorf("error decoding JSON response after PUT: %s", err) } } err = resp.Body.Close() if err != nil { - return fmt.Errorf("error closing HTTP PUT response body: %s", err) + return nil, fmt.Errorf("error closing HTTP PUT response body: %s", err) } - return nil + return resp.Header, nil } // OpenApiDeleteItem is a low level OpenAPI client function to perform DELETE request for any item. diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index a1936a7a0..dab401ed5 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -56,6 +56,9 @@ var endpointMinApiVersions = map[string]string{ 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+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve: "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+ @@ -129,6 +132,10 @@ var endpointElevatedApiVersions = map[string][]string{ //"35.0", // Introduced support "37.1", // Added MaxImplicitRight property in DefinedEntityType }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: { + //"35.0", // Introduced support + "37.0", // Added metadata support + }, } // checkOpenApiEndpointCompatibility checks if VCD version (to which the client is connected) is sufficient to work with diff --git a/types/v56/constants.go b/types/v56/constants.go index 47b23ed21..8083103b6 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -391,6 +391,9 @@ const ( OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID OpenApiEndpointRdeInterfaces = "interfaces/" OpenApiEndpointRdeEntityTypes = "entityTypes/" + OpenApiEndpointRdeEntities = "entities/" + OpenApiEndpointRdeEntitiesTypes = "entities/types/" + OpenApiEndpointRdeEntitiesResolve = "entities/%s/resolve" // NSX-T ALB related endpoints diff --git a/types/v56/openapi.go b/types/v56/openapi.go index e904b4725..e86497877 100644 --- a/types/v56/openapi.go +++ b/types/v56/openapi.go @@ -453,3 +453,15 @@ type DefinedEntityType struct { 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 } + +// DefinedEntity describes an instance of a defined entity type. +type DefinedEntity struct { + ID string `json:"id,omitempty"` // The id of the defined entity in URN format + EntityType string `json:"entityType,omitempty"` // The URN ID of the defined entity type that the entity is an instance of. This is a read-only field + Name string `json:"name,omitempty"` // The name of the defined entity + ExternalId string `json:"externalId,omitempty"` // An external entity's id that this entity may have a relation to. + Entity map[string]interface{} `json:"entity,omitempty"` // A JSON value representation. The JSON will be validated against the schema of the DefinedEntityType that the entity is an instance of + State *string `json:"state,omitempty"` // Every entity is created in the "PRE_CREATED" state. Once an entity is ready to be validated against its schema, it will transition in another state - RESOLVED, if the entity is valid according to the schema, or RESOLUTION_ERROR otherwise. If an entity in an "RESOLUTION_ERROR" state is updated, it will transition to the inital "PRE_CREATED" state without performing any validation. If its in the "RESOLVED" state, then it will be validated against the entity type schema and throw an exception if its invalid + Owner *OpenApiReference `json:"owner,omitempty"` // The owner of the defined entity + Org *OpenApiReference `json:"org,omitempty"` // The organization of the defined entity. +}