diff --git a/vsphere/internal/helper/contentlibrary/content_library_helper.go b/vsphere/internal/helper/contentlibrary/content_library_helper.go index 906de31b2..9e0807ba6 100644 --- a/vsphere/internal/helper/contentlibrary/content_library_helper.go +++ b/vsphere/internal/helper/contentlibrary/content_library_helper.go @@ -3,16 +3,18 @@ package contentlibrary import ( "context" "fmt" + "log" + "path/filepath" + "time" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/datastore" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/provider" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/structure" "github.com/vmware/govmomi" "github.com/vmware/govmomi/vapi/library" "github.com/vmware/govmomi/vapi/rest" "github.com/vmware/govmomi/vapi/vcenter" - "log" - "path/filepath" - "time" ) // FromName accepts a Content Library name and returns a Library object. @@ -48,15 +50,37 @@ func FromID(c *rest.Client, id string) (*library.Library, error) { } // CreateLibrary creates a Content Library. -func CreateLibrary(c *rest.Client, name string, description string, backings []library.StorageBackings) (string, error) { +func CreateLibrary(d *schema.ResourceData, restclient *rest.Client, backings []library.StorageBackings) (string, error) { + name := d.Get("name").(string) log.Printf("[DEBUG] contentlibrary.CreateLibrary: Creating content library %s", name) - clm := library.NewManager(c) + clm := library.NewManager(restclient) ctx := context.TODO() lib := library.Library{ - Description: description, + Description: d.Get("description").(string), Name: name, Storage: backings, - Type: "LOCAL", // govmomi only supports LOCAL library creation + Type: "LOCAL", + } + if len(d.Get("publication").([]interface{})) > 0 { + publication := d.Get("publication").([]interface{})[0].(map[string]interface{}) + lib.Publication = &library.Publication{ + Published: structure.BoolPtr(publication["published"].(bool)), + AuthenticationMethod: publication["authentication_method"].(string), + UserName: publication["username"].(string), + Password: publication["password"].(string), + } + } + if len(d.Get("subscription").([]interface{})) > 0 { + subscription := d.Get("subscription").([]interface{})[0].(map[string]interface{}) + lib.Subscription = &library.Subscription{ + AutomaticSyncEnabled: structure.BoolPtr(subscription["automatic_sync"].(bool)), + OnDemand: structure.BoolPtr(subscription["on_demand"].(bool)), + AuthenticationMethod: subscription["authentication_method"].(string), + UserName: subscription["username"].(string), + Password: subscription["password"].(string), + SubscriptionURL: subscription["subscription_url"].(string), + } + lib.Type = "SUBSCRIBED" } id, err := clm.CreateLibrary(ctx, lib) if err != nil { @@ -66,7 +90,7 @@ func CreateLibrary(c *rest.Client, name string, description string, backings []l return id, nil } -func UpdateLibrary(c *rest.Client, ol *library.Library, name string, description string, backings []library.StorageBackings) error { +func updateLibrary(c *rest.Client, ol *library.Library, name string, description string, backings []library.StorageBackings) error { // Not currently supported in govmomi return nil } @@ -195,8 +219,8 @@ func DeleteLibraryItem(c *rest.Client, item *library.Item) error { func ExpandStorageBackings(c *govmomi.Client, d *schema.ResourceData) ([]library.StorageBackings, error) { log.Printf("[DEBUG] contentlibrary.ExpandStorageBackings: Expanding OVF storage backing.") sb := []library.StorageBackings{} - for _, dsId := range d.Get("storage_backing").(*schema.Set).List() { - ds, err := datastore.FromID(c, dsId.(string)) + for _, dsID := range d.Get("storage_backing").(*schema.Set).List() { + ds, err := datastore.FromID(c, dsID.(string)) if err != nil { return nil, provider.ProviderError(d.Id(), "ExpandStorageBackings", err) } @@ -209,8 +233,39 @@ func ExpandStorageBackings(c *govmomi.Client, d *schema.ResourceData) ([]library return sb, nil } +// FlattenPublication takes a Publication sub resource and sets it in ResourceData. +func FlattenPublication(d *schema.ResourceData, publication *library.Publication) error { + if publication == nil { + return nil + } + log.Printf("[DEBUG] contentlibrary.FlattenPublication: Flattening publication.") + flatPublication := map[string]interface{}{} + flatPublication["authentication_method"] = publication.AuthenticationMethod + flatPublication["username"] = publication.UserName + flatPublication["password"] = d.Get("publication.0.password").(string) + flatPublication["publish_url"] = publication.PublishURL + flatPublication["published"] = publication.Published + return d.Set("publication", []interface{}{flatPublication}) +} + +// FlattenSubscription takes a Subscription sub resource and sets it in ResourceData. +func FlattenSubscription(d *schema.ResourceData, subscription *library.Subscription) error { + if subscription == nil { + return nil + } + log.Printf("[DEBUG] contentlibrary.FlattenSubscription: Flattening subscription.") + flatSubscription := map[string]interface{}{} + flatSubscription["authentication_method"] = subscription.AuthenticationMethod + flatSubscription["username"] = subscription.UserName + flatSubscription["password"] = d.Get("subscription.0.password").(string) + flatSubscription["subscription_url"] = subscription.SubscriptionURL + flatSubscription["automatic_sync"] = subscription.AutomaticSyncEnabled + flatSubscription["on_demand"] = subscription.OnDemand + return d.Set("subscription", []interface{}{flatSubscription}) +} + // FlattenStorageBackings takes a list of StorageBackings, and returns a list of datastore IDs. -func FlattenStorageBackings(sb []library.StorageBackings) []string { +func FlattenStorageBackings(d *schema.ResourceData, sb []library.StorageBackings) error { log.Printf("[DEBUG] contentlibrary.FlattenStorageBackings: Flattening OVF storage backing.") sbl := []string{} for _, backing := range sb { @@ -219,7 +274,7 @@ func FlattenStorageBackings(sb []library.StorageBackings) []string { } } log.Printf("[DEBUG] contentlibrary.FlattenStorageBackings: Successfully flattened OVF storage backing.") - return sbl + return d.Set("storage_backing", sbl) } // MapStorageDevices maps disks defined in the OVF to datastores. diff --git a/vsphere/resource_vsphere_content_library.go b/vsphere/resource_vsphere_content_library.go index 664e2aeeb..ff2a67ffe 100644 --- a/vsphere/resource_vsphere_content_library.go +++ b/vsphere/resource_vsphere_content_library.go @@ -35,6 +35,94 @@ func resourceVSphereContentLibrary() *schema.Resource { Description: "The name of the content library.", Elem: &schema.Schema{Type: schema.TypeString}, }, + "publication": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Computed: true, + Description: "Publication configuration for content library.", + ConflictsWith: []string{"subscription"}, + MaxItems: 1, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "authentication_method": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "NONE", + }, + "username": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + }, + "password": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + }, + "published": { + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: false, + }, + "publish_url": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + }, + }, + }, + }, + "subscription": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Publication configuration for content library.", + ConflictsWith: []string{"publication"}, + MaxItems: 1, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "authentication_method": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "NONE", + }, + "subscription_url": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "NONE", + }, + "username": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + }, + "password": { + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + }, + "on_demand": { + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: true, + }, + "automatic_sync": { + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: false, + }, + }, + }, + }, }, } } @@ -47,31 +135,34 @@ func resourceVSphereContentLibraryRead(d *schema.ResourceData, meta interface{}) if strings.Contains(err.Error(), "404 Not Found") { d.SetId("") return nil - } else { - return err } + return err } d.SetId(lib.ID) - sb := contentlibrary.FlattenStorageBackings(lib.Storage) - d.Set("name", lib.Name) - d.Set("description", lib.Description) - err = d.Set("storage_backing", sb) - if err != nil { + if err = contentlibrary.FlattenPublication(d, lib.Publication); err != nil { return err } + if err = contentlibrary.FlattenSubscription(d, lib.Subscription); err != nil { + return err + } + if err = contentlibrary.FlattenStorageBackings(d, lib.Storage); err != nil { + return err + } + d.Set("name", lib.Name) + d.Set("description", lib.Description) log.Printf("[DEBUG] resourceVSphereContentLibraryRead : Content Library (%s) read is complete", d.Id()) return nil } func resourceVSphereContentLibraryCreate(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] resourceVSphereContentLibraryCreate : Beginning Content Library (%s) creation", d.Get("name").(string)) - vc := meta.(*VSphereClient).vimClient - rc := meta.(*VSphereClient).restClient - backings, err := contentlibrary.ExpandStorageBackings(vc, d) + vimClient := meta.(*VSphereClient).vimClient + restClient := meta.(*VSphereClient).restClient + backings, err := contentlibrary.ExpandStorageBackings(vimClient, d) if err != nil { return err } - id, err := contentlibrary.CreateLibrary(rc, d.Get("name").(string), d.Get("description").(string), backings) + id, err := contentlibrary.CreateLibrary(d, restClient, backings) if err != nil { return err } diff --git a/vsphere/resource_vsphere_content_library_test.go b/vsphere/resource_vsphere_content_library_test.go index 95f2cf4a9..0ec85a63e 100644 --- a/vsphere/resource_vsphere_content_library_test.go +++ b/vsphere/resource_vsphere_content_library_test.go @@ -31,7 +31,7 @@ func TestAccResourceVSphereContentLibrary_basic(t *testing.T) { "vsphere_content_library.library", "description", regexp.MustCompile("Library Description"), ), testAccResourceVSphereContentLibraryDescription(regexp.MustCompile("Library Description")), - testAccResourceVSphereContentLibraryName(regexp.MustCompile("ContentLibrary_test")), + testAccResourceVSphereContentLibraryName(regexp.MustCompile("testacc_content_library")), ), }, { @@ -44,6 +44,54 @@ func TestAccResourceVSphereContentLibrary_basic(t *testing.T) { }) } +func TestAccResourceVSphereContentLibrary_subscribed(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereContentLibraryPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereContentLibraryCheckExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereContentLibraryConfig_subscribed(), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr( + "vsphere_content_library.library", "id", regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"), + ), + resource.TestMatchResourceAttr( + "vsphere_content_library.library", "description", regexp.MustCompile("Library Description"), + ), + testAccResourceVSphereContentLibraryDescription(regexp.MustCompile("Library Description")), + testAccResourceVSphereContentLibraryName(regexp.MustCompile("testacc_subscribed")), + ), + }, + }, + }) +} +func TestAccResourceVSphereContentLibrary_authenticated(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereContentLibraryPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereContentLibraryCheckExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereContentLibraryConfig_authenticated(), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr( + "vsphere_content_library.library", "id", regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"), + ), + ), + }, + }, + }) +} + func testAccResourceVSphereContentLibraryPreCheck(t *testing.T) { if os.Getenv("TF_VAR_VSPHERE_DATACENTER") == "" { t.Skip("set TF_VAR_VSPHERE_DATACENTER to run vsphere_content_library acceptance tests") @@ -79,32 +127,76 @@ func testAccResourceVSphereContentLibraryName(expected *regexp.Regexp) resource. } } -func testAccResourceVSphereContentLibraryConfig() string { - return fmt.Sprintf(` -variable "datacenter" { - type = "string" - default = "%s" +func testAccResourceVSphereContentLibraryConfig_base() string { + return testhelper.CombineConfigs(testhelper.ConfigDataRootDC1(), testhelper.ConfigDataRootHost1(), testhelper.ConfigDataRootDS1(), testhelper.ConfigDataRootHost2(), testhelper.ConfigResDS1(), testhelper.ConfigDataRootComputeCluster1(), testhelper.ConfigResResourcePool1(), testhelper.ConfigDataRootPortGroup1()) } +func testAccResourceVSphereContentLibraryConfig_authenticated() string { + return fmt.Sprintf(` +%s +resource "vsphere_content_library" "library_published" { + name = "testacc_published" + storage_backing = [ data.vsphere_datastore.rootds1.id ] + description = "Library Description" + publication { + authentication_method = "BASIC" + username = "vcsp" + password = "Password123!" + published = true + } +} -data "vsphere_datacenter" "dc" { - name = data.vsphere_datacenter.rootdc1.name +resource "vsphere_content_library" "library" { + name = "testacc_subscribed" + storage_backing = [ data.vsphere_datastore.rootds1.id ] + description = "Library Description" + subscription { + authentication_method = "BASIC" + username = "vcsp" + password = "Password123!" + subscription_url = vsphere_content_library.library_published.publication.0.publish_url + } +} +`, + testAccResourceVSphereContentLibraryConfig_base()) +} +func testAccResourceVSphereContentLibraryConfig_subscribed() string { + return fmt.Sprintf(` +%s + +resource "vsphere_content_library" "library_published" { + name = "testacc_published" + storage_backing = [ data.vsphere_datastore.rootds1.id ] + description = "Library Description" + publication { + published = true + } } -data "vsphere_datastore" "ds" { - datacenter_id = data.vsphere_datacenter.rootdc1.id - name = var.datastore +resource "vsphere_content_library" "library" { + name = "testacc_subscribed" + storage_backing = [ data.vsphere_datastore.rootds1.id ] + description = "Library Description" + subscription { + subscription_url = vsphere_content_library.library_published.publication.0.publish_url + } +} +`, + testAccResourceVSphereContentLibraryConfig_base()) } +func testAccResourceVSphereContentLibraryConfig() string { + return fmt.Sprintf(` +%s + resource "vsphere_content_library" "library" { - name = "ContentLibrary_test" - storage_backing = [ data.vsphere_datastore.ds.id ] + name = "testacc_content_library" + storage_backing = [ data.vsphere_datastore.rootds1.id ] description = "Library Description" } `, - testhelper.CombineConfigs(testhelper.ConfigDataRootDC1(), testhelper.ConfigDataRootHost1(), testhelper.ConfigDataRootHost2(), testhelper.ConfigResDS1(), testhelper.ConfigDataRootComputeCluster1(), testhelper.ConfigResResourcePool1(), testhelper.ConfigDataRootPortGroup1()), - ) + testAccResourceVSphereContentLibraryConfig_base()) } func testAccResourceVSphereContentLibraryCheckExists(expected bool) resource.TestCheckFunc { diff --git a/website/docs/r/content_library.html.markdown b/website/docs/r/content_library.html.markdown index 9f85e59ac..b7cfd14f0 100644 --- a/website/docs/r/content_library.html.markdown +++ b/website/docs/r/content_library.html.markdown @@ -46,15 +46,28 @@ The following arguments are supported: * `storage_backing` - (Required) The [managed object reference ID][docs-about-morefs] on which to store Content Library items. * `description` - (Optional) A description of the Content Library. +* `publication` - (Optional) Options to publish a local Content Library. + * `authentication_method` - (Optional) Method to authenticate users. Must be `NONE` or `BASIC`. + * `username` - (Optional) User name subscribers log in with. Currently can only be `vcsp`. + * `password` - (Optional) Password subscribers log in with. + * `published` - (Optional) Bool determining if Content Library is published. +* `subscription` - (Optional) Options to publish a local Content Library. + * `subscription_url` - (Required) URL of remote Content Library. + * `authentication_method` - (Optional) Method to log into remote Content Library. Must be `NONE` or `BASIC`. + * `username` - (Optional) User name to log in with. + * `password` - (Optional) Password to log in with. + * `automatic_sync` - (Optional) Enable automatic synchronization with the external content library. + * `on_demand` - (Optional) Download all library content immediately. [docs-about-morefs]: /docs/providers/vsphere/index.html#use-of-managed-object-references-by-the-vsphere-provider ## Attribute Reference -The only attribute this resource exports is the `id` of the resource, which is -a combination of the [managed object reference ID][docs-about-morefs] of the -cluster, and the name of the virtual machine group. + +* `id` The [managed object reference ID][docs-about-morefs] of the Content Library, and the name of the virtual machine group. +* `subscription` + * `publish_url` - URL to remotely access the published Content Library. ## Importing