diff --git a/.changes/v1.0.0/2-feature.md b/.changes/v1.0.0/2-feature.md new file mode 100644 index 0000000..34d17fe --- /dev/null +++ b/.changes/v1.0.0/2-feature.md @@ -0,0 +1,4 @@ +* **New Resource:** `vcfa_vcenter` to manage VMware Cloud Foundation Automation Tenant Manager + vCenter servers [GH-2] +* **New Data Source:** `vcfa_vcenter` to read VMware Cloud Foundation Automation Tenant Manager + vCenter servers [GH-2] diff --git a/.changes/v1.0.0/2-notes.md b/.changes/v1.0.0/2-notes.md new file mode 100644 index 0000000..62535ec --- /dev/null +++ b/.changes/v1.0.0/2-notes.md @@ -0,0 +1,2 @@ +- Add generic resource management functionality in `resource_generic_crud.go` [GH-2] +- Add leftover removal mechanism that makes `make cleanup` work [GH-2] diff --git a/go.mod b/go.mod index 3776b51..997e215 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.3 require ( github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 - github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.14 + github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.16 ) require ( diff --git a/go.sum b/go.sum index 11cbd51..acd7f67 100644 --- a/go.sum +++ b/go.sum @@ -149,8 +149,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.14 h1:zY0f4JXNsK2z04DuoQ9VLeRw0FZq1sRCRrmSeKog/ps= -github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.14/go.mod h1:68KHsVns52dsq/w5JQYzauaU/+NAi1FmCxhBrFc/VoQ= +github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.16 h1:EnsrEachJsb3i3jwvy9pYS1Avim5FvUzmoLTkq//OWM= +github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.16/go.mod h1:68KHsVns52dsq/w5JQYzauaU/+NAi1FmCxhBrFc/VoQ= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/vcfa/config.go b/vcfa/config.go index f94fc12..274c814 100644 --- a/vcfa/config.go +++ b/vcfa/config.go @@ -186,6 +186,12 @@ func (c *Config) Client() (*VCDClient, error) { if err != nil { return nil, fmt.Errorf("something went wrong during authentication: %s", err) } + + // Require API V40 (TM starting API version) to be present + if vcdClient.Client.APIVCDMaxVersionIs("< 40") { + return nil, fmt.Errorf("unsupported API version, at least v40 required") + } + cachedVCDClients.Lock() cachedVCDClients.conMap[checksum] = cachedConnection{initTime: time.Now(), connection: vcdClient} cachedVCDClients.Unlock() diff --git a/vcfa/datasource_vcfa_vcenter.go b/vcfa/datasource_vcfa_vcenter.go new file mode 100644 index 0000000..ff3cb21 --- /dev/null +++ b/vcfa/datasource_vcfa_vcenter.go @@ -0,0 +1,101 @@ +package vcfa + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/vmware/go-vcloud-director/v3/govcd" + "github.com/vmware/go-vcloud-director/v3/types/v56" +) + +func datasourceVcfaVcenter() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceVcfaVcenterRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Name of %s", labelVcfaVirtualCenter), + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("URL of %s", labelVcfaVirtualCenter), + }, + "username": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Username of %s", labelVcfaVirtualCenter), + }, + "is_enabled": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("Should the %s be enabled", labelVcfaVirtualCenter), + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Description of %s", labelVcfaVirtualCenter), + }, + "has_proxy": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("A flag that shows if %s has proxy defined", labelVcfaVirtualCenter), + }, + "is_connected": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("A flag that shows if %s is connected", labelVcfaVirtualCenter), + }, + "mode": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Mode of %s", labelVcfaVirtualCenter), + }, + "connection_status": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Listener state of %s", labelVcfaVirtualCenter), + }, + "cluster_health_status": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Mode of %s", labelVcfaVirtualCenter), + }, + "vcenter_version": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Version of %s", labelVcfaVirtualCenter), + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("%s UUID", labelVcfaVirtualCenter), + }, + "vcenter_host": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("%s hostname", labelVcfaVirtualCenter), + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "vCenter status", + }, + }, + } +} + +func datasourceVcfaVcenterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + c := dsReadConfig[*govcd.VCenter, types.VSphereVirtualCenter]{ + entityLabel: labelVcfaVirtualCenter, + getEntityFunc: vcdClient.GetVCenterByName, + stateStoreFunc: setVcenterData, + } + return readDatasource(ctx, d, meta, c) +} diff --git a/vcfa/provider.go b/vcfa/provider.go index dc20fce..19ebe5c 100644 --- a/vcfa/provider.go +++ b/vcfa/provider.go @@ -33,10 +33,11 @@ func Resources(nameRegexp string, includeDeprecated bool) (map[string]*schema.Re var globalDataSourceMap = map[string]*schema.Resource{ "vcfa_tm_version": datasourceVcfaTmVersion(), // 1.0 + "vcfa_vcenter": datasourceVcfaVcenter(), // 1.0 } var globalResourceMap = map[string]*schema.Resource{ - "vcfa_deleteme": resourceVcfaDeleteme(), // TODO: VCFA: Delete this (and associated doc) once there's a resource ready + "vcfa_vcenter": resourceVcfaVcenter(), // 1.0 } // Provider returns a terraform.ResourceProvider. diff --git a/vcfa/provider_unit_test.go b/vcfa/provider_unit_test.go index dac8533..52edbb0 100644 --- a/vcfa/provider_unit_test.go +++ b/vcfa/provider_unit_test.go @@ -131,8 +131,8 @@ func TestVcfaResources(t *testing.T) { }, { name: "MatchExactResourceName", - args: args{nameRegexp: "vcfa_deleteme", includeDeprecated: false}, // TODO: VCFA: Change this to correct resource once there's one - wantLen: 1, // should return only one because exact name was given + args: args{nameRegexp: "vcfa_vcenter", includeDeprecated: false}, + wantLen: 1, // should return only one because exact name was given lenOnly: true, wantErr: false, }, diff --git a/vcfa/remove_leftovers_test.go b/vcfa/remove_leftovers_test.go index 503af85..0846070 100644 --- a/vcfa/remove_leftovers_test.go +++ b/vcfa/remove_leftovers_test.go @@ -2,15 +2,127 @@ package vcfa import ( "fmt" + "regexp" + "strings" + "github.com/vmware/go-vcloud-director/v3/govcd" ) // This file contains routines that clean up the test suite after failed tests +// entityDef is the definition of an entity (to be either deleted or kept) +// with an optional comment +type entityDef struct { + Type string `json:"type"` + Name string `json:"name"` + Comment string `json:"comment,omitempty"` + NameRegexp *regexp.Regexp +} + +// entityList is a collection of entityDef +type entityList []entityDef + +// doNotDelete contains a list of entities that should not be deleted, +// despite having a name that starts with `Test` or `test` +var doNotDelete = entityList{ + // {Type: "vcfa_xxx", Name: "test_entity", Comment: "loaded with provisioning"}, +} + +// alsoDelete contains a list of entities that should be removed , in addition to the ones +// found by name matching +// Add to this list if you ever get an entity left behind by a test +var alsoDelete = entityList{ + // {Type: "vcfa_xxx", Name: "custom-name", Comment: "manually created"}, +} + +// isTest is a regular expression that tells if an entity needs to be deleted +var isTest = regexp.MustCompile(`^[Tt]est`) + +// alwaysShow lists the resources that will always be shown +var alwaysShow = []string{ + "vcfa_vcenter", +} + func removeLeftovers(govcdClient *govcd.VCDClient, verbose bool) error { if verbose { fmt.Printf("Start leftovers removal\n") } + // -------------------------------------------------------------- + // vCenters + // -------------------------------------------------------------- + if govcdClient.Client.IsSysAdmin { + allVcs, err := govcdClient.GetAllVCenters(nil) + if err != nil { + return fmt.Errorf("error retrieving provider vCenters: %s", err) + } + for _, vc := range allVcs { + toBeDeleted := shouldDeleteEntity(alsoDelete, doNotDelete, vc.VSphereVCenter.Name, "vcfa_vcenter", 0, verbose) + if toBeDeleted { + fmt.Printf("\t REMOVING vCenter %s\n", vc.VSphereVCenter.Name) + err = vc.Disable() + if err != nil { + return fmt.Errorf("error disabling %s '%s': %s", labelVcfaVirtualCenter, vc.VSphereVCenter.Name, err) + } + err := vc.Delete() + if err != nil { + return fmt.Errorf("error deleting %s '%s': %s", labelVcfaVirtualCenter, vc.VSphereVCenter.Name, err) + } + } + } + } + return nil } + +// shouldDeleteEntity checks whether a given entity is to be deleted, either by its name +// or by its inclusion in one of the entity lists +func shouldDeleteEntity(alsoDelete, doNotDelete entityList, name, entityType string, level int, verbose bool) bool { + inclusion := "" + exclusion := "" + // 1. First requirement to be deleted: the entity name starts with 'Test' or 'test' + toBeDeleted := isTest.MatchString(name) + if inList(alsoDelete, name, entityType) { + toBeDeleted = true + // 2. If the entity was in the additional deletion list, regardless of the name, + // it is marked for deletion, with a "+", indicating that it was selected for deletion because of the + // deletion list + inclusion = " +" + } + if inList(doNotDelete, name, entityType) { + toBeDeleted = false + // 3. If a file, normally marked for deletion, is found in the keep list, + // its deletion status is revoked, and it is marked with a "-", indicating that it was excluded + // for deletion because of the keep list + exclusion = " -" + } + tabs := strings.Repeat("\t", level) + format := tabs + "[%s] %s (%s%s%s)\n" + deletionText := "DELETE" + if !toBeDeleted { + deletionText = "keep" + } + + // 4. Show the entity. If it is to be deleted, it will always be shown + if toBeDeleted || contains(alwaysShow, entityType) { + if verbose { + fmt.Printf(format, entityType, name, deletionText, inclusion, exclusion) + } + } + return toBeDeleted +} + +// inList shows whether a given entity is included in an entityList +func inList(list entityList, name, entityType string) bool { + for _, element := range list { + // Compare by names + if element.Name == name && element.Type == entityType { + return true + } + // Compare by possible regexp values + if element.NameRegexp != nil && element.NameRegexp.MatchString(name) { + return true + } + } + return false +} diff --git a/vcfa/resource_generic_crud.go b/vcfa/resource_generic_crud.go new file mode 100644 index 0000000..13324e7 --- /dev/null +++ b/vcfa/resource_generic_crud.go @@ -0,0 +1,318 @@ +package vcfa + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/vmware/go-vcloud-director/v3/govcd" + "github.com/vmware/go-vcloud-director/v3/util" +) + +// crudConfig defines a generic approach for managing Terraform resources where the parent entity is +// a standard OpenAPI entity and the outer entity should satisfy 'updateDeleter' type constraint +// (have 'Update' and 'Delete' pointer receiver methods) +type crudConfig[O updateDeleter[O, I], I any] struct { + // entityLabel to use + entityLabel string + + // getTypeFunc is responsible for converting schema fields to inner type + getTypeFunc func(*VCDClient, *schema.ResourceData) (*I, error) + // stateStoreFunc is responsible for storing state + stateStoreFunc func(vcdClient *VCDClient, d *schema.ResourceData, outerType O) error + + // createFunc is the function that can create an outer entity based on inner entity config + // (which is created by 'getTypeFunc') + createFunc func(config *I) (O, error) + + // createAsyncFunc is the function that can create an outer entity based on inner entity config + // (which is created by 'getTypeFunc'). It differs from createFunc in a way that it can capture + // failing task and store resource ID so that the entity becomes tainted instead of losing a + // reference. + createAsyncFunc func(config *I) (*govcd.Task, error) + + // resourceReadFunc that will be executed from Create and Update functions + resourceReadFunc func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics + + // getEntityFunc is a function that retrieves the entity + // It will use ID for resources and Name for data sources + getEntityFunc func(idOrName string) (O, error) + + // preCreateHooks will be executed before the entity is created + preCreateHooks []schemaHook + + // preUpdateHooks will be executed before submitting the data for update + preUpdateHooks []outerEntityHookInnerEntityType[O, *I] + + // preDeleteHooks will be executed before the entity is deleted + preDeleteHooks []outerEntityHook[O] + + // readHooks that will be executed after the entity is read, but before it is stored in state + readHooks []outerEntityHook[O] +} + +// updateDeleter is a type constraint to match only entities that have Update and Delete methods +type updateDeleter[O any, I any] interface { + Update(*I) (O, error) + Delete() error +} + +// outerEntityHook defines a type for hook that can be fed into generic CRUD operations +type outerEntityHook[O any] func(O) error + +// schemaHook defines a type for hook that can be fed into generic CRUD operations +type schemaHook func(*VCDClient, *schema.ResourceData) error + +// outerEntityHookInnerEntityType defines a type for hook that will provide retrieved outer entity +// with a newly computed inner entity type (useful for modifying update body before submitting it) +type outerEntityHookInnerEntityType[O, I any] func(*schema.ResourceData, O, I) error + +func createResource[O updateDeleter[O, I], I any](ctx context.Context, d *schema.ResourceData, meta interface{}, c crudConfig[O, I]) diag.Diagnostics { + err := createResourceValidator(c) + if err != nil { + return diag.Errorf("validation failed: %s", err) + } + + vcdClient := meta.(*VCDClient) + t, err := c.getTypeFunc(vcdClient, d) + if err != nil { + return diag.Errorf("error getting %s type on create: %s", c.entityLabel, err) + } + + err = execSchemaHook(vcdClient, d, c.preCreateHooks) + if err != nil { + return diag.Errorf("error executing pre-create %s hooks: %s", c.entityLabel, err) + } + + var createdEntity O + + // If Async creation function is specified - attempt to parse it this way + if c.createAsyncFunc != nil { + task, err := c.createAsyncFunc(t) + if err != nil { + return diag.Errorf("error creating async %s: %s", c.entityLabel, err) + } + + err = task.WaitTaskCompletion() + if err != nil { + if task != nil && task.Task != nil { + util.Logger.Printf("[DEBUG] entity '%s' task with ID '%s' failed. Attempting to recover ID", c.entityLabel, task.Task.ID) + // Try to see if there is an owner + if task.Task.Owner != nil && task.Task.Owner.ID != "" { + util.Logger.Printf("[DEBUG] entity '%s' task with ID '%s' failed. Found owner ID %s", c.entityLabel, task.Task.ID, task.Task.Owner.ID) + + // Storing entity ID + failedEntityId := task.Task.Owner.ID + d.SetId(failedEntityId) + + return diag.Errorf("error creating entity %s. Storing tainted resources ID %s. Task error: %s", c.entityLabel, failedEntityId, err) + } + } + + return diag.Errorf("task error while creating async %s. Owner ID not found: %s", c.entityLabel, err) + } + createdEntity, err = c.getEntityFunc(task.Task.Owner.ID) + if err != nil { + return diag.Errorf("error retrieving %s after successful task: %s", c.entityLabel, err) + } + } + + if c.createAsyncFunc == nil { + createdEntity, err = c.createFunc(t) + if err != nil { + return diag.Errorf("error creating %s: %s", c.entityLabel, err) + } + } + + err = c.stateStoreFunc(vcdClient, d, createdEntity) + if err != nil { + return diag.Errorf("error storing %s to state during create: %s", c.entityLabel, err) + } + + return c.resourceReadFunc(ctx, d, meta) +} + +func createResourceValidator[O updateDeleter[O, I], I any](c crudConfig[O, I]) error { + if c.createFunc != nil && c.createAsyncFunc != nil { + return fmt.Errorf("only one of 'createFunc' and 'createAsyncFunc can be specified for %s creation", c.entityLabel) + } + return nil +} + +func updateResource[O updateDeleter[O, I], I any](ctx context.Context, d *schema.ResourceData, meta interface{}, c crudConfig[O, I]) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + t, err := c.getTypeFunc(vcdClient, d) + if err != nil { + return diag.Errorf("error getting %s type on update: %s", c.entityLabel, err) + } + + if d.Id() == "" { + return diag.Errorf("empty id for updating %s", c.entityLabel) + } + + retrievedEntity, err := c.getEntityFunc(d.Id()) + if err != nil { + return diag.Errorf("error getting %s for update: %s", c.entityLabel, err) + } + + err = execUpdateEntityHookWithNewInnerType(d, retrievedEntity, t, c.preUpdateHooks) + if err != nil { + return diag.Errorf("error executing pre-update %s hooks: %s", c.entityLabel, err) + } + + _, err = retrievedEntity.Update(t) + if err != nil { + return diag.Errorf("error updating %s with ID: %s", c.entityLabel, err) + } + + return c.resourceReadFunc(ctx, d, meta) +} + +func readResource[O updateDeleter[O, I], I any](_ context.Context, d *schema.ResourceData, meta interface{}, c crudConfig[O, I]) diag.Diagnostics { + retrievedEntity, err := c.getEntityFunc(d.Id()) + if err != nil { + if govcd.ContainsNotFound(err) { + util.Logger.Printf("[DEBUG] entity '%s' with ID '%s' not found. Removing from state", c.entityLabel, d.Id()) + d.SetId("") + return nil + } + return diag.Errorf("error getting %s: %s", c.entityLabel, err) + } + + err = execEntityHook(retrievedEntity, c.readHooks) + if err != nil { + return diag.Errorf("error executing read %s hooks: %s", c.entityLabel, err) + } + + vcdClient := meta.(*VCDClient) + err = c.stateStoreFunc(vcdClient, d, retrievedEntity) + if err != nil { + return diag.Errorf("error storing %s to state during resource read: %s", c.entityLabel, err) + } + + return nil +} + +func deleteResource[O updateDeleter[O, I], I any](_ context.Context, d *schema.ResourceData, _ interface{}, c crudConfig[O, I]) diag.Diagnostics { + retrievedEntity, err := c.getEntityFunc(d.Id()) + if err != nil { + return diag.Errorf("error getting %s for delete: %s", c.entityLabel, err) + } + + err = execEntityHook(retrievedEntity, c.preDeleteHooks) + if err != nil { + return diag.Errorf("error executing pre-delete %s hooks: %s", c.entityLabel, err) + } + + err = retrievedEntity.Delete() + if err != nil { + return diag.Errorf("error deleting %s with ID '%s': %s", c.entityLabel, d.Id(), err) + } + + return nil +} + +func execSchemaHook(vcdClient *VCDClient, d *schema.ResourceData, runList []schemaHook) error { + if len(runList) == 0 { + util.Logger.Printf("[DEBUG] No hooks to execute") + return nil + } + + var err error + for i := range runList { + err = runList[i](vcdClient, d) + if err != nil { + return fmt.Errorf("error executing hook: %s", err) + } + + } + + return nil +} + +func execEntityHook[O any](outerEntity O, runList []outerEntityHook[O]) error { + if len(runList) == 0 { + util.Logger.Printf("[DEBUG] No hooks to execute") + return nil + } + + var err error + for i := range runList { + err = runList[i](outerEntity) + if err != nil { + return fmt.Errorf("error executing hook: %s", err) + } + + } + + return nil +} + +func execUpdateEntityHookWithNewInnerType[O, I any](d *schema.ResourceData, outerEntity O, newInnerEntity I, runList []outerEntityHookInnerEntityType[O, I]) error { + if len(runList) == 0 { + util.Logger.Printf("[DEBUG] No hooks to execute") + return nil + } + + var err error + for i := range runList { + err = runList[i](d, outerEntity, newInnerEntity) + if err != nil { + return fmt.Errorf("error executing hook: %s", err) + } + + } + + return nil +} + +// dsReadConfig is a generic type that can be used for data sources. It differs from `crudConfig` in +// the sense that it does not have `updateDeleter` type parameter constraint. This is needed for +// such data sources that have no API to Update and/or Delete an entity, but instead are read-only +// entities. +type dsReadConfig[O any, I any] struct { + // entityLabel to use + entityLabel string + + // stateStoreFunc is responsible for storing state + stateStoreFunc func(vcdClient *VCDClient, d *schema.ResourceData, outerType O) error + + // getEntityFunc is a function that retrieves the entity + // It will use ID for resources and Name for data sources + getEntityFunc func(idOrName string) (O, error) + + // preReadHooks will be executed before the entity is created + preReadHooks []schemaHook + + // overrideDefaultNameField permits to override default field ('name') that passed to + // getEntityFunc. The field must be a string (schema.TypeString) + overrideDefaultNameField string +} + +// readDatasource will read a data source by a 'name' field in Terraform schema +func readDatasource[O any, I any](_ context.Context, d *schema.ResourceData, meta interface{}, c dsReadConfig[O, I]) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + err := execSchemaHook(vcdClient, d, c.preReadHooks) + if err != nil { + return diag.Errorf("error executing pre-read %s hooks: %s", c.entityLabel, err) + } + + fieldName := "name" + if c.overrideDefaultNameField != "" { + fieldName = c.overrideDefaultNameField + util.Logger.Printf("[DEBUG] Overriding %s field 'name' to '%s' for datasource lookup", c.entityLabel, c.overrideDefaultNameField) + } + entityName := d.Get(fieldName).(string) + retrievedEntity, err := c.getEntityFunc(entityName) + if err != nil { + return diag.Errorf("error getting %s by Name '%s': %s", c.entityLabel, entityName, err) + } + + err = c.stateStoreFunc(vcdClient, d, retrievedEntity) + if err != nil { + return diag.Errorf("error storing %s to state during data source read: %s", c.entityLabel, err) + } + + return nil +} diff --git a/vcfa/resource_vcfa_vcenter.go b/vcfa/resource_vcfa_vcenter.go new file mode 100644 index 0000000..2294eda --- /dev/null +++ b/vcfa/resource_vcfa_vcenter.go @@ -0,0 +1,375 @@ +package vcfa + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/vmware/go-vcloud-director/v3/govcd" + "github.com/vmware/go-vcloud-director/v3/types/v56" + "github.com/vmware/go-vcloud-director/v3/util" +) + +const labelVcfaVirtualCenter = "Tenant Manager vCenter Server" + +func resourceVcfaVcenter() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVcfaVcenterCreate, + ReadContext: resourceVcfaVcenterRead, + UpdateContext: resourceVcfaVcenterUpdate, + DeleteContext: resourceVcfaVcenterDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceVcfaVcenterImport, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Name of %s", labelVcfaVirtualCenter), + }, + "url": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("URL including port of %s", labelVcfaVirtualCenter), + }, + "auto_trust_certificate": { + Type: schema.TypeBool, + Required: true, + ForceNew: true, + Description: fmt.Sprintf("Defines if the %s certificate should automatically be trusted", labelVcfaVirtualCenter), + }, + "refresh_vcenter_on_read": { + Type: schema.TypeBool, + Optional: true, + Description: fmt.Sprintf("Defines if the %s should be refreshed on every read operation", labelVcfaVirtualCenter), + }, + "refresh_policies_on_read": { + Type: schema.TypeBool, + Optional: true, + Description: fmt.Sprintf("Defines if the %s should refresh Policies on every read operation", labelVcfaVirtualCenter), + }, + "username": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Username of %s", labelVcfaVirtualCenter), + }, + "password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: fmt.Sprintf("Password of %s", labelVcfaVirtualCenter), + }, + "is_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: fmt.Sprintf("Should the %s be enabled", labelVcfaVirtualCenter), + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf("Description of %s", labelVcfaVirtualCenter), + }, + "has_proxy": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("A flag that shows if %s has proxy defined", labelVcfaVirtualCenter), + }, + "is_connected": { + Type: schema.TypeBool, + Computed: true, + Description: fmt.Sprintf("A flag that shows if %s is connected", labelVcfaVirtualCenter), + }, + "mode": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Mode of %s", labelVcfaVirtualCenter), + }, + "connection_status": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Listener state of %s", labelVcfaVirtualCenter), + }, + "cluster_health_status": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Mode of %s", labelVcfaVirtualCenter), + }, + "vcenter_version": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Version of %s", labelVcfaVirtualCenter), + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("%s UUID", labelVcfaVirtualCenter), + }, + "vcenter_host": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("%s hostname", labelVcfaVirtualCenter), + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "vCenter status", + }, + }, + } +} + +func getVcenterType(_ *VCDClient, d *schema.ResourceData) (*types.VSphereVirtualCenter, error) { + t := &types.VSphereVirtualCenter{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Url: d.Get("url").(string), + Username: d.Get("username").(string), + Password: d.Get("password").(string), + IsEnabled: d.Get("is_enabled").(bool), + } + + return t, nil +} + +func setVcenterData(_ *VCDClient, d *schema.ResourceData, v *govcd.VCenter) error { + if v == nil || v.VSphereVCenter == nil { + return fmt.Errorf("nil object for %s", labelVcfaVirtualCenter) + } + + dSet(d, "name", v.VSphereVCenter.Name) + dSet(d, "description", v.VSphereVCenter.Description) + dSet(d, "url", v.VSphereVCenter.Url) + dSet(d, "username", v.VSphereVCenter.Username) + // dSet(d, "password", v.VSphereVCenter.Password) // password is never returned, + dSet(d, "is_enabled", v.VSphereVCenter.IsEnabled) + + dSet(d, "has_proxy", v.VSphereVCenter.HasProxy) + dSet(d, "is_connected", v.VSphereVCenter.IsConnected) + dSet(d, "mode", v.VSphereVCenter.Mode) + dSet(d, "connection_status", v.VSphereVCenter.ListenerState) + dSet(d, "cluster_health_status", v.VSphereVCenter.ClusterHealthStatus) + dSet(d, "vcenter_version", v.VSphereVCenter.VcVersion) + dSet(d, "uuid", v.VSphereVCenter.Uuid) + host, err := url.Parse(v.VSphereVCenter.Url) + if err != nil { + return fmt.Errorf("error parsing URL for storing 'vcenter_host': %s", err) + } + dSet(d, "vcenter_host", host.Host) + + // Status is a derivative value that was present in XML Query API, but is no longer maintained + // The value was derived from multiple fields based on a complex logic. Instead, evaluating if + // vCenter is ready for operations, would be to rely on `is_enabled`, `is_connected` and + // optionally `cluster_health_status` fields. + // + // The `status` is a rough approximation of this value + dSet(d, "status", "NOT_READY") + if v.VSphereVCenter.IsConnected && v.VSphereVCenter.ListenerState == "CONNECTED" { + dSet(d, "status", "READY") + } + + d.SetId(v.VSphereVCenter.VcId) + + return nil +} + +func resourceVcfaVcenterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + c := crudConfig[*govcd.VCenter, types.VSphereVirtualCenter]{ + entityLabel: labelVcfaVirtualCenter, + getTypeFunc: getVcenterType, + stateStoreFunc: setVcenterData, + createAsyncFunc: vcdClient.CreateVcenterAsync, + getEntityFunc: vcdClient.GetVCenterById, + resourceReadFunc: resourceVcfaVcenterRead, + // certificate should be trusted for the vCenter to work + preCreateHooks: []schemaHook{autoTrustHostCertificate("url", "auto_trust_certificate")}, + } + return createResource(ctx, d, meta, c) +} + +func resourceVcfaVcenterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // return immediately if only flags are updated + if !d.HasChangesExcept("refresh_vcenter_on_read", "refresh_policies_on_read") { + return nil + } + + vcdClient := meta.(*VCDClient) + c := crudConfig[*govcd.VCenter, types.VSphereVirtualCenter]{ + entityLabel: labelVcfaVirtualCenter, + getTypeFunc: getVcenterType, + getEntityFunc: vcdClient.GetVCenterById, + resourceReadFunc: resourceVcfaVcenterRead, + } + + return updateResource(ctx, d, meta, c) +} + +func resourceVcfaVcenterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + // prefetch vCenter so that vc.VSphereVCenter.IsEnabled and vc.VSphereVCenter.IsConnected flags + // can be verified and avoid triggering refreshes if VC is disconnected + vc, err := vcdClient.GetVCenterById(d.Id()) + if err != nil { + if govcd.ContainsNotFound(err) { + d.SetId("") + } + return diag.Errorf("error retrieving vCenter by Id: %s", err) + } + + // TODO: TM: remove this block and use the commented one within crudConfig below. + // Retrieval endpoints by Name and by ID return differently formated url (the by Id one returns + // URL with port http://host:443, while the one by name - doesn't). Using the same getByName to + // match format everywhere + fakeGetById := func(_ string) (*govcd.VCenter, error) { + return vcdClient.GetVCenterByName(vc.VSphereVCenter.Name) + } + + shouldRefresh := d.Get("refresh_vcenter_on_read").(bool) + shouldRefreshPolicies := d.Get("refresh_policies_on_read").(bool) + shouldWaitForListenerStatus := true + + // There is no way to detect if a resource is 'tainted' ('d.State().Tainted' is not reliable), + // but if a resource is not connected and is not enabled - there is no point in refreshing + // anything + // It will help in the case when invalid configuration is supplied and a creation task fails as + // a tainted resource has to be read before releasing it + if !vc.VSphereVCenter.IsEnabled && !vc.VSphereVCenter.IsConnected { + shouldRefresh = false + shouldRefreshPolicies = false + shouldWaitForListenerStatus = false + } + c := crudConfig[*govcd.VCenter, types.VSphereVirtualCenter]{ + entityLabel: labelVcfaVirtualCenter, + // getEntityFunc: vcdClient.GetVCenterById,// TODO: TM: use this function + getEntityFunc: fakeGetById, // TODO: TM: remove this function + stateStoreFunc: setVcenterData, + readHooks: []outerEntityHook[*govcd.VCenter]{ + // TODO: TM ensure that the vCenter listener state is "CONNECTED" before triggering + // refresh as it will fail otherwise. At the moment it has a delay before it becomes + // CONNECTED after creation task succeeds. It should not be needed once vCenter creation + // task ensures that the listener is connected. + shouldWaitForListenerStatusConnected(shouldWaitForListenerStatus), + + refreshVcenter(shouldRefresh), // vCenter read can optionally trigger "refresh" operation + refreshVcenterPolicy(shouldRefreshPolicies), // vCenter read can optionally trigger "refresh policies" operation + }, + } + return readResource(ctx, d, meta, c) +} + +func resourceVcfaVcenterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + c := crudConfig[*govcd.VCenter, types.VSphereVirtualCenter]{ + entityLabel: labelVcfaVirtualCenter, + getEntityFunc: vcdClient.GetVCenterById, + preDeleteHooks: []outerEntityHook[*govcd.VCenter]{disableVcenter}, // vCenter must be disabled before deletion + } + + return deleteResource(ctx, d, meta, c) +} + +func resourceVcfaVcenterImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + vcdClient := meta.(*VCDClient) + + v, err := vcdClient.GetVCenterByName(d.Id()) + if err != nil { + return nil, fmt.Errorf("error retrieving %s by name: %s", labelVcfaVirtualCenter, err) + } + + d.SetId(v.VSphereVCenter.VcId) + return []*schema.ResourceData{d}, nil +} + +// disableVcenter disables vCenter which is usefull before deletion as a non-disabled vCenter cannot +// be removed +func disableVcenter(v *govcd.VCenter) error { + if v.VSphereVCenter.IsEnabled { + return v.Disable() + } + return nil +} + +// refreshVcenter triggers refresh on vCenter which is useful for reloading some of the vCenter +// components like Supervisors +func refreshVcenter(execute bool) outerEntityHook[*govcd.VCenter] { + return func(v *govcd.VCenter) error { + if execute { + err := v.RefreshVcenter() + if err != nil { + return fmt.Errorf("error refreshing vCenter: %s", err) + } + } + return nil + } +} + +// refreshVcenterPolicy triggers refresh on vCenter which is useful for reloading some of the +// vCenter components like Supervisors +func refreshVcenterPolicy(execute bool) outerEntityHook[*govcd.VCenter] { + return func(v *govcd.VCenter) error { + if execute { + err := v.RefreshStorageProfiles() + if err != nil { + return fmt.Errorf("error refreshing Storage Policies: %s", err) + } + } + return nil + } +} + +// TODO: TM: should not be required because a successful vCenter creation task should work +func shouldWaitForListenerStatusConnected(shouldWait bool) func(v *govcd.VCenter) error { + return func(v *govcd.VCenter) error { + if !shouldWait { + return nil + } + for c := 0; c < 20; c++ { + err := v.Refresh() + if err != nil { + return fmt.Errorf("error refreshing vCenter: %s", err) + } + + if v.VSphereVCenter.ListenerState == "CONNECTED" { + return nil + } + + time.Sleep(2 * time.Second) + } + + return fmt.Errorf("failed waiting for listener state to become 'CONNECTED', got '%s'", v.VSphereVCenter.ListenerState) + } +} + +// autoTrustHostCertificate can automatically add host certificate to trusted ones +// * urlSchemaFieldName - Terraform schema field (TypeString) name that contains URL of entity +// * trustSchemaFieldName - Terraform schema field (TypeBool) name that defines if the certificate should be trusted +// Note. It will not add new entry if the certificate is already trusted +func autoTrustHostCertificate(urlSchemaFieldName, trustSchemaFieldName string) schemaHook { + return func(vcdClient *VCDClient, d *schema.ResourceData) error { + shouldExecute := d.Get(trustSchemaFieldName).(bool) + if !shouldExecute { + util.Logger.Printf("[DEBUG] Skipping certificate trust execution as '%s' is false", trustSchemaFieldName) + return nil + } + schemaUrl := d.Get(urlSchemaFieldName).(string) + parsedUrl, err := url.Parse(schemaUrl) + if err != nil { + return fmt.Errorf("error parsing provided url '%s': %s", schemaUrl, err) + } + + _, err = vcdClient.AutoTrustCertificate(parsedUrl) + if err != nil { + return fmt.Errorf("error trusting '%s' certificate: %s", schemaUrl, err) + } + + return nil + } +} diff --git a/vcfa/resource_vcfa_vcenter_test.go b/vcfa/resource_vcfa_vcenter_test.go new file mode 100644 index 0000000..7f69e19 --- /dev/null +++ b/vcfa/resource_vcfa_vcenter_test.go @@ -0,0 +1,223 @@ +//go:build tm || ALL || functional + +package vcfa + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccVcfaVcenter(t *testing.T) { + preTestChecks(t) + skipIfNotSysAdmin(t) + + if !testConfig.Tm.CreateVcenter { + t.Skipf("Skipping vCenter creation") + } + + var params = StringMap{ + "Org": testConfig.Tm.Org, + "VcenterUsername": testConfig.Tm.VcenterUsername, + "VcenterPassword": testConfig.Tm.VcenterPassword, + "VcenterUrl": testConfig.Tm.VcenterUrl, + + "Testname": t.Name(), + + "Tags": "tm", + } + testParamsNotEmpty(t, params) + + configText1 := templateFill(testAccVcfaVcenterStep1, params) + params["FuncName"] = t.Name() + "-step2" + configText2 := templateFill(testAccVcfaVcenterStep2, params) + + params["FuncName"] = t.Name() + "-step3" + configText3 := templateFill(testAccVcfaVcenterStep3, params) + + params["FuncName"] = t.Name() + "-step4" + configText4 := templateFill(testAccVcfaVcenterStep4DS, params) + + debugPrintf("#[DEBUG] CONFIGURATION step1: %s\n", configText1) + debugPrintf("#[DEBUG] CONFIGURATION step2: %s\n", configText2) + debugPrintf("#[DEBUG] CONFIGURATION step3: %s\n", configText3) + debugPrintf("#[DEBUG] CONFIGURATION step4: %s\n", configText4) + if vcfaShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: configText1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "id"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "name", t.Name()), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "description", ""), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "is_enabled", "true"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "has_proxy", "false"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "cluster_health_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "is_connected"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "connection_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "mode"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "uuid"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "vcenter_version"), + ), + }, + { + Config: configText2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "id"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "name", t.Name()+"-rename"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "is_enabled", "false"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "description", "description from Terraform"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "has_proxy", "false"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "cluster_health_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "is_connected"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "connection_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "mode"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "uuid"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "vcenter_version"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "status", "READY"), + ), + }, + { + Config: configText3, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "id"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "name", t.Name()), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "description", ""), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "is_enabled", "true"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "has_proxy", "false"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "cluster_health_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "is_connected"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "connection_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "mode"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "uuid"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "vcenter_version"), + ), + }, + { + ResourceName: "vcfa_vcenter.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: params["Testname"].(string), + ImportStateVerifyIgnore: []string{"password", "auto_trust_certificate", "refresh_vcenter_on_read", "refresh_policies_on_read"}, + }, + { + Config: configText4, + Check: resource.ComposeTestCheckFunc( + resourceFieldsEqual("data.vcfa_vcenter.test", "vcfa_vcenter.test", []string{"%"}), + ), + }, + }, + }) + + postTestChecks(t) +} + +const testAccVcfaVcenterStep1 = ` +resource "vcfa_vcenter" "test" { + name = "{{.Testname}}" + url = "{{.VcenterUrl}}" + auto_trust_certificate = true + refresh_vcenter_on_read = true + refresh_policies_on_read = true + username = "{{.VcenterUsername}}" + password = "{{.VcenterPassword}}" + is_enabled = true +} +` + +const testAccVcfaVcenterStep2 = ` +resource "vcfa_vcenter" "test" { + name = "{{.Testname}}-rename" + description = "description from Terraform" + auto_trust_certificate = true + url = "{{.VcenterUrl}}" + username = "{{.VcenterUsername}}" + password = "{{.VcenterPassword}}" + is_enabled = false +} +` + +const testAccVcfaVcenterStep3 = ` +resource "vcfa_vcenter" "test" { + name = "{{.Testname}}" + url = "{{.VcenterUrl}}" + auto_trust_certificate = true + username = "{{.VcenterUsername}}" + password = "{{.VcenterPassword}}" + is_enabled = true +} +` + +const testAccVcfaVcenterStep4DS = testAccVcfaVcenterStep3 + ` +data "vcfa_vcenter" "test" { + name = vcfa_vcenter.test.name +} +` + +func TestAccVcfaVcenterInvalid(t *testing.T) { + preTestChecks(t) + skipIfNotSysAdmin(t) + + // test fails on purpose + if vcfaShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + var params = StringMap{ + "Org": testConfig.Tm.Org, + "VcenterUsername": testConfig.Tm.VcenterUsername, + "VcenterPassword": "invalid", + "VcenterUrl": testConfig.Tm.VcenterUrl, + + "Testname": t.Name(), + + "Tags": "tm", + } + testParamsNotEmpty(t, params) + + configText1 := templateFill(testAccVcfaVcenterStep1, params) + params["FuncName"] = t.Name() + "-step2" + params["VcenterPassword"] = testConfig.Tm.VcenterPassword + configText2 := templateFill(testAccVcfaVcenterStep1, params) + + debugPrintf("#[DEBUG] CONFIGURATION step1: %s\n", configText1) + debugPrintf("#[DEBUG] CONFIGURATION step2: %s\n", configText2) + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: configText1, + ExpectError: regexp.MustCompile(`Failed to connect to the vCenter`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "id"), + ), + }, + { + Config: configText2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "id"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "name", t.Name()), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "is_enabled", "true"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "cluster_health_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "is_connected"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "connection_status"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "mode"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "uuid"), + resource.TestCheckResourceAttrSet("vcfa_vcenter.test", "vcenter_version"), + resource.TestCheckResourceAttr("vcfa_vcenter.test", "status", "READY"), + ), + }, + }, + }) + + postTestChecks(t) +} diff --git a/vcfa/structure.go b/vcfa/structure.go index 7612e7a..b773707 100644 --- a/vcfa/structure.go +++ b/vcfa/structure.go @@ -1,16 +1,8 @@ package vcfa -import "os" - -// Checks if a file exists -func fileExists(filename string) bool { - f, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - fileMode := f.Mode() - return fileMode.IsRegular() -} +import ( + "os" +) // contains returns true if `sliceToSearch` contains `searched`. Returns false otherwise. func contains(sliceToSearch []string, searched string) bool { @@ -23,3 +15,13 @@ func contains(sliceToSearch []string, searched string) bool { } return found } + +// Checks if a file exists +func fileExists(filename string) bool { + f, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + fileMode := f.Mode() + return fileMode.IsRegular() +} diff --git a/vcfa/testcheck_funcs_test.go b/vcfa/testcheck_funcs_test.go new file mode 100644 index 0000000..dedc6da --- /dev/null +++ b/vcfa/testcheck_funcs_test.go @@ -0,0 +1,50 @@ +//go:build api || functional || tm || ALL + +package vcfa + +import ( + "fmt" + "reflect" + "slices" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// resourceFieldsEqual checks if secondObject has all the fields and their values set as the +// firstObject except `[]excludeFields`. This is very useful to check if data sources have all +// the same values as resources +func resourceFieldsEqual(firstObject, secondObject string, excludeFields []string) resource.TestCheckFunc { + return resourceFieldsEqualCustom(firstObject, secondObject, excludeFields, slices.Contains) +} + +func resourceFieldsEqualCustom(firstObject, secondObject string, excludeFields []string, exclusionChecker func(list []string, str string) bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource1, ok := s.RootModule().Resources[firstObject] + if !ok { + return fmt.Errorf("unable to find %s", firstObject) + } + + resource2, ok := s.RootModule().Resources[secondObject] + if !ok { + return fmt.Errorf("unable to find %s", secondObject) + } + + for fieldName := range resource1.Primary.Attributes { + // Do not validate the fields marked for exclusion + if excludeFields != nil && exclusionChecker(excludeFields, fieldName) { + continue + } + + if vcfaTestVerbose { + fmt.Printf("field %s %s (value %s) and %s (value %s))\n", fieldName, firstObject, + resource1.Primary.Attributes[fieldName], secondObject, resource2.Primary.Attributes[fieldName]) + } + if !reflect.DeepEqual(resource1.Primary.Attributes[fieldName], resource2.Primary.Attributes[fieldName]) { + return fmt.Errorf("field %s differs in resources %s (value %s) and %s (value %s)", + fieldName, firstObject, resource1.Primary.Attributes[fieldName], secondObject, resource2.Primary.Attributes[fieldName]) + } + } + return nil + } +} diff --git a/website/docs/d/vcenter.html.markdown b/website/docs/d/vcenter.html.markdown new file mode 100644 index 0000000..20fc48b --- /dev/null +++ b/website/docs/d/vcenter.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "vcfa" +page_title: "VMware Cloud Foundation Automation: vcfa_vcenter" +sidebar_current: "docs-vcfa-data-source-vcenter" +description: |- + Provides a data source for reading vCenters attached to VMware Cloud Foundation Automation Tenant Manager. +--- + +# vcfa\_vcenter + +Provides a data source for reading vCenters attached to VMware Cloud Foundation Automation Tenant Manager. + +## Example Usage + +```hcl +data "vcfa_vcenter" "vc" { + name = "vcenter-one" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) vCenter name + +## Attribute reference + +All attributes defined in +[`vcfa_vcenter`](/providers/vmware/vcfa/latest/docs/resources/vcenter#attribute-reference) are +supported. diff --git a/website/docs/r/deleteme.html.markdown b/website/docs/r/deleteme.html.markdown deleted file mode 100644 index e69de29..0000000 diff --git a/website/docs/r/vcenter.html.markdown b/website/docs/r/vcenter.html.markdown new file mode 100644 index 0000000..1dec542 --- /dev/null +++ b/website/docs/r/vcenter.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "vcfa" +page_title: "VMware Cloud Foundation Automation: vcfa_vcenter" +sidebar_current: "docs-vcfa-resource-vcenter" +description: |- + Provides a resource to manage vCenters in VMware Cloud Foundation Automation Tenant Manager. +--- + +# vcfa\_vcenter + +Provides a resource to manage vCenters in VMware Cloud Foundation Automation Tenant Manager. + +~> Only `System Administrator` can create this resource. + +## Example Usage + +```hcl +resource "vcfa_vcenter" "test" { + name = "my-vCenter" + url = "https://host" + auto_trust_certificate = true + refresh_vcenter_on_read = true + username = "admin@vsphere.local" + password = "CHANGE-ME" + is_enabled = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A name for vCenter server +* `description` - (Optional) An optional description for vCenter server +* `username` - (Required) A username for authenticating to vCenter server +* `password` - (Required) A password for authenticating to vCenter server +* `refresh_vcenter_on_read` - (Optional) An optional flag to trigger refresh operation on the + underlying vCenter on every read. This might take some time, but can help to load up new artifacts + from vCenter (e.g. Supervisors). This operation is visible as a new task in UI. Update is a no-op. + It may be useful after adding vCenter or if new infrastructure is added to vCenter. Default + `false`. +* `refresh_policies_on_read` - (Optional) An optional flag to trigger policy refresh operation on + the underlying vCenter on every read. This might take some time, but can help to load up new + artifacts from vCenter (e.g. Storage Policies). Update is a no-op. This operation is visible as a + new task in UI. It may be useful after adding vCenter or if new infrastructure is added to + vCenter. Default `false`. +* `url` - (Required) An URL of vCenter server +* `auto_trust_certificate` - (Required) Defines if the certificate of a given vCenter server should + automatically be added to trusted certificate store. **Note:** not having the certificate trusted + will cause malfunction. +* `is_enabled` - (Optional) Defines if the vCenter is enabled. Default `true`. The vCenter must + always be disabled before removal (this resource will disable it automatically on destroy). + + +## Attribute Reference + +The following attributes are exported on this resource: + +* `has_proxy` - Indicates that a proxy exists within vCloud Director that proxies this vCenter + server for access by authorized end-users +* `is_connected` - Defines if the vCenter server is connected. +* `mode` - One of `NONE`, `IAAS` (scoped to the provider), `SDDC` (scoped to tenants), `MIXED` (both + uses are possible) +* `connection_status` - `INITIAL`, `INVALID_SETTINGS`, `UNSUPPORTED`, `DISCONNECTED`, `CONNECTING`, + `CONNECTED_SYNCING`, `CONNECTED`, `STOP_REQ`, `STOP_AND_PURGE_REQ`, `STOP_ACK` +* `cluster_health_status` - Cluster health status. One of `GRAY` , `RED` , `YELLOW` , `GREEN` +* `version` - vCenter version +* `uuid` - UUID of vCenter +* `vcenter_host` - Host of vCenter server +* `status` - Status can be `READY` or `NOT_READY`. It is a derivative field of `is_connected` and + `connection_status` so relying on those fields could be more precise. + +## Importing + +~> **Note:** The current implementation of Terraform import can only import resources into the +state. It does not generate configuration. However, an experimental feature in Terraform 1.5+ allows +also code generation. See [Importing resources][importing-resources] for more information. + +An existing vCenter configuration can be [imported][docs-import] into this resource via supplying +path for it. An example is below: + +[docs-import]: https://www.terraform.io/docs/import/ + +``` +terraform import vcfa_vcenter.imported my-vcenter +``` + +The above would import the `my-vcenter` vCenter settings that are defined at provider level. diff --git a/website/vcfa.erb b/website/vcfa.erb index e1bf42e..e9ef63b 100644 --- a/website/vcfa.erb +++ b/website/vcfa.erb @@ -24,13 +24,16 @@