From d279451facd91f3c9b55433928171f5636568f3d Mon Sep 17 00:00:00 2001 From: Pierre Lafievre Date: Tue, 14 Mar 2023 20:59:03 +0100 Subject: [PATCH] F #414: add marketplace resource --- go.mod | 3 +- go.sum | 8 +- opennebula/provider.go | 1 + opennebula/resource_opennebula_marketplace.go | 1122 +++++++++++++++++ .../resource_opennebula_marketplace_test.go | 114 ++ website/docs/r/marketplace.html.markdown | 124 ++ 6 files changed, 1368 insertions(+), 4 deletions(-) create mode 100644 opennebula/resource_opennebula_marketplace.go create mode 100644 opennebula/resource_opennebula_marketplace_test.go create mode 100644 website/docs/r/marketplace.html.markdown diff --git a/go.mod b/go.mod index d72efe7bd..dac00579e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/OpenNebula/terraform-provider-opennebula go 1.18 require ( - github.com/OpenNebula/one/src/oca/go/src/goca v0.0.0-20230301133003-197f04efa071 + github.com/OpenNebula/one/src/oca/go/src/goca v0.0.0-20230912160253-4673721a21ca github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 @@ -34,6 +34,7 @@ require ( github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 13577e361..e654c123d 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/OpenNebula/one/src/oca/go/src/goca v0.0.0-20230301133003-197f04efa071 h1:qxsdY1BZDslfn3RIpfLwirhj1SCrlK/vD9gVqMIiYv0= -github.com/OpenNebula/one/src/oca/go/src/goca v0.0.0-20230301133003-197f04efa071/go.mod h1:dvAwZi1Aol7eu6BENzHtl8ztGBkacB9t/fJj+fYk+Xg= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= @@ -124,8 +122,9 @@ github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e h1:JZPIpxHmcXiQn101f6P github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -155,11 +154,14 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= diff --git a/opennebula/provider.go b/opennebula/provider.go index 17086bd1c..7684af126 100644 --- a/opennebula/provider.go +++ b/opennebula/provider.go @@ -105,6 +105,7 @@ func Provider() *schema.Provider { "opennebula_cluster": resourceOpennebulaCluster(), "opennebula_host": resourceOpennebulaHost(), "opennebula_datastore": resourceOpennebulaDatastore(), + "opennebula_marketplace": resourceOpennebulaMarketPlace(), }, ConfigureContextFunc: providerConfigure, diff --git a/opennebula/resource_opennebula_marketplace.go b/opennebula/resource_opennebula_marketplace.go new file mode 100644 index 000000000..6d6628e28 --- /dev/null +++ b/opennebula/resource_opennebula_marketplace.go @@ -0,0 +1,1122 @@ +package opennebula + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/OpenNebula/one/src/oca/go/src/goca" + "github.com/OpenNebula/one/src/oca/go/src/goca/parameters" + "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/marketplace" +) + +const ( + One string = "one" + Http = "http" + S3 = "s3" + LinuxContainers = "linuxcontainers" + TurnkeyLinux = "turnkeylinux" + DockerHub = "dockerhub" +) + +var S3Types = []string{"aws", "minio", "ceph"} + +var defaultMarketMinTimeout = 20 +var defaultMarketTimeout = time.Duration(defaultHostMinTimeout) * time.Minute + +// Common schema to linux containers LXC and Turnkey linux +func commonBackendSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "endpoint_url": { + Type: schema.TypeString, + Optional: true, + Description: "The base URL of the Market", + }, + "roofs_image_size": { + Type: schema.TypeInt, + Optional: true, + Description: "Size in MB for the image holding the rootfs", + }, + "filesystem": { + Type: schema.TypeString, + Optional: true, + Description: "Filesystem used for the image", + }, + "image_block_file_format": { + Type: schema.TypeString, + Optional: true, + Description: "Image block file format", + }, + "skip_untested": { + Type: schema.TypeBool, + Optional: true, + Description: "Include only apps with support for context", + }, + } +} + +func resourceOpennebulaMarketPlace() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceOpennebulaMarketPlaceCreate, + ReadContext: resourceOpennebulaMarketPlaceRead, + UpdateContext: resourceOpennebulaMarketPlaceUpdate, + DeleteContext: resourceOpennebulaMarketPlaceDelete, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(defaultMarketTimeout), + Update: schema.DefaultTimeout(defaultMarketTimeout), + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + CustomizeDiff: SetTagsDiff, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the marketplace", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the marketplace", + }, + "permissions": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Permissions for the marketplace (in Unix format, owner-group-other, use-manage-admin)", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if len(value) != 3 { + errors = append(errors, fmt.Errorf("%q has specify 3 permission sets: owner-group-other", k)) + } + + all := true + for _, c := range strings.Split(value, "") { + if c < "0" || c > "7" { + all = false + } + } + if !all { + errors = append(errors, fmt.Errorf("Each character in %q should specify a Unix-like permission set with a number from 0 to 7", k)) + } + + return + }, + }, + "uid": { + Type: schema.TypeInt, + Computed: true, + Description: "ID of the user that will own the marketplace", + }, + "gid": { + Type: schema.TypeInt, + Computed: true, + Description: "ID of the group that will own the marketplace", + }, + "uname": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the user that will own the marketplace", + }, + "gname": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the group that will own the marketplace", + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Allow to enable or disable the market place", + }, + "one": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_url": { + Type: schema.TypeString, + Optional: true, + Description: "The marketplace endpoint url", + }, + }, + }, + ConflictsWith: []string{"http", "s3", "lxc", "turnkey", "dockerhub"}, + }, + "http": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_url": { + Type: schema.TypeString, + Required: true, + Description: "Base URL of the Marketplace HTTP endpoint", + }, + "path": { + Type: schema.TypeString, + Required: true, + Description: "Absolute directory path to place images in the front-end or in the hosts pointed at by storage_bridge_list", + }, + "storage_bridge_list": { + Type: schema.TypeSet, + Optional: true, + Description: "List of servers to access the public directory", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MinItems: 1, + }, + }, + }, + ConflictsWith: []string{"one", "s3", "lxc", "turnkey", "dockerhub"}, + }, + "s3": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + Description: "Type of the s3 backend: aws, ceph, minio", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + + if inArray(v.(string), S3Types) < 0 { + errors = append(errors, fmt.Errorf("s3 backend \"type\" must be one of: %s", strings.Join(S3Types, ","))) + } + + return + }, + }, + "access_key_id": { + Type: schema.TypeString, + Required: true, + Description: "The access key of the S3 user", + }, + "secret_access_key": { + Type: schema.TypeString, + Required: true, + Description: "The secret key of the S3 user", + }, + "bucket": { + Type: schema.TypeString, + Required: true, + Description: "The bucket where the files will be stored", + }, + "region": { + Type: schema.TypeString, + Required: true, + Description: "The region to connect to. Any value will work with Ceph-S3", + }, + "endpoint_url": { + Type: schema.TypeString, + Optional: true, + Description: "Only required when connecteing to a service other than Amazon S3", + }, + "total_size": { + Type: schema.TypeInt, + Optional: true, + Default: 1048576, + Description: "Define the total size of the marketplace in MB.", + }, + "read_block_length": { + Type: schema.TypeInt, + Optional: true, + Default: 100, + Description: "Split the file into chunks of this size in MB", + }, + }, + }, + ConflictsWith: []string{"one", "http", "lxc", "turnkey", "dockerhub"}, + }, + "lxc": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: mergeSchemas( + commonBackendSchema(), + map[string]*schema.Schema{ + "cpu": { + Type: schema.TypeInt, + Optional: true, + Description: "VM template CPU", + }, + "vcpu": { + Type: schema.TypeInt, + Optional: true, + Description: "VM template VCPU", + }, + "memory": { + Type: schema.TypeInt, + Optional: true, + Description: "VM template memory", + }, + "privileged": { + Type: schema.TypeBool, + Optional: true, + Description: "Secrurity mode of the Linux Container", + }, + }), + }, + ConflictsWith: []string{"one", "http", "s3", "turnkey", "dockerhub"}, + }, + "turnkey": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: commonBackendSchema(), + }, + ConflictsWith: []string{"one", "http", "s3", "lxc", "dockerhub"}, + }, + "dockerhub": { + Type: schema.TypeBool, + Optional: true, + ConflictsWith: []string{"one", "http", "s3", "lxc", "turnkey"}, + }, + "tags": tagsSchema(), + "default_tags": defaultTagsSchemaComputed(), + "tags_all": tagsSchemaComputed(), + "template_section": templateSectionSchema(), + }, + } +} + +func getMarketPlaceController(d *schema.ResourceData, meta interface{}) (*goca.MarketPlaceController, error) { + config := meta.(*Configuration) + controller := config.Controller + var gc *goca.MarketPlaceController + + if d.Id() != "" { + gid, err := strconv.ParseUint(d.Id(), 10, 0) + if err != nil { + return nil, err + } + gc = controller.MarketPlace(int(gid)) + } + + return gc, nil +} + +func resourceOpennebulaMarketPlaceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + config := meta.(*Configuration) + controller := config.Controller + + var diags diag.Diagnostics + + tpl, diags := generateMarketplaceTemplate(d, meta) + if len(diags) > 0 { + return diags + } + + tagsInterface := d.Get("tags").(map[string]interface{}) + for k, v := range tagsInterface { + tpl.AddPair(strings.ToUpper(k), v) + } + + // add default tags if they aren't overriden + if len(config.defaultTags) > 0 { + for k, v := range config.defaultTags { + key := strings.ToUpper(k) + p, _ := tpl.GetPair(key) + if p != nil { + continue + } + tpl.AddPair(key, v) + } + } + + log.Printf("[DEBUG] create marketplace with template: %s", tpl.String()) + + marketplaceID, err := controller.MarketPlaces().Create(tpl.String()) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to create the marketplace", + Detail: err.Error(), + }) + return diags + } + d.SetId(fmt.Sprintf("%d", marketplaceID)) + + mpc := controller.MarketPlace(marketplaceID) + + // update permisions + if perms, ok := d.GetOk("permissions"); ok { + err = mpc.Chmod(permissionUnix(perms.(string))) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to change permissions", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + + // manage enabled/disabled state + disabled := d.Get("disabled").(bool) + if disabled { + err := mpc.Enable(!disabled) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to enable/disable the marketplace", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + timeout := d.Timeout(schema.TimeoutCreate) + _, err = waitForMarketplaceStates(ctx, mpc, timeout, []string{marketplace.Enabled.String()}, []string{marketplace.Disabled.String()}) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to wait marketplace to be in DISABLED state", + Detail: fmt.Sprintf("marketplace marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + + return resourceOpennebulaMarketPlaceRead(ctx, d, meta) +} + +func generateMarketplaceTemplate(d *schema.ResourceData, meta interface{}) (*marketplace.Template, diag.Diagnostics) { + + tpl := marketplace.NewTemplate() + var diags diag.Diagnostics + + name, ok := d.GetOk("name") + if ok { + tpl.AddPair("NAME", name.(string)) + } + + description, ok := d.GetOk("description") + if ok { + tpl.AddPair("DESCRIPTION", description.(string)) + } + + diags = generateBackendOne(d, meta, tpl) + if len(diags) > 0 { + return nil, diags + } + + diags = generateBackendHttp(d, meta, tpl) + if len(diags) > 0 { + return nil, diags + } + + diags = generateBackendS3(d, meta, tpl) + if len(diags) > 0 { + return nil, diags + } + + diags = generateBackendLXC(d, meta, tpl) + if len(diags) > 0 { + return nil, diags + } + + diags = generateBackendTurnkey(d, meta, tpl) + if len(diags) > 0 { + return nil, diags + } + + backendDockerhub := d.Get("dockerhub").(bool) + if backendDockerhub { + tpl.AddPair("MARKET_MAD", DockerHub) + } + + return tpl, nil +} + +func generateBackendOne(d *schema.ResourceData, meta interface{}, tpl *marketplace.Template) diag.Diagnostics { + var diags diag.Diagnostics + + backendOneList := d.Get("one").(*schema.Set).List() + if len(backendOneList) > 0 { + + backendOneIf := backendOneList[0] + backendOne := backendOneIf.(map[string]interface{}) + + tpl.AddPair("MARKET_MAD", One) + + endpoint := backendOne["endpoint_url"].(string) + if len(endpoint) > 0 { + tpl.AddPair("ENDPOINT", endpoint) + + } + + } + + return diags +} + +func generateBackendHttp(d *schema.ResourceData, meta interface{}, tpl *marketplace.Template) diag.Diagnostics { + var diags diag.Diagnostics + + backendHttpList := d.Get("http").(*schema.Set).List() + if len(backendHttpList) > 0 { + + tpl.AddPair("MARKET_MAD", Http) + + backendHttpIf := backendHttpList[0] + backendHttp := backendHttpIf.(map[string]interface{}) + + endpoint := backendHttp["endpoint_url"].(string) + tpl.AddPair("BASE_URL", endpoint) + + path := backendHttp["path"].(string) + tpl.AddPair("PUBLIC_DIR", path) + + bridgeList := backendHttp["storage_bridge_list"].(*schema.Set).List() + if len(bridgeList) > 0 { + bridgeListStr := bridgeList[0].(string) + for _, bridge := range bridgeList[1:] { + bridgeListStr += " " + bridge.(string) + } + tpl.AddPair("BRIDGE_LIST", bridgeListStr) + } + + } + + return diags +} + +func generateBackendS3(d *schema.ResourceData, meta interface{}, tpl *marketplace.Template) diag.Diagnostics { + var diags diag.Diagnostics + + backendS3List := d.Get("s3").(*schema.Set).List() + if len(backendS3List) > 0 { + + backendS3If := backendS3List[0] + backendS3 := backendS3If.(map[string]interface{}) + + tpl.AddPair("MARKET_MAD", S3) + + accessKeyID := backendS3["access_key_id"].(string) + tpl.AddPair("ACCESS_KEY_ID", accessKeyID) + + secretAccessKey := backendS3["secret_access_key"].(string) + tpl.AddPair("SECRET_ACCESS_KEY", secretAccessKey) + + bucket := backendS3["bucket"].(string) + tpl.AddPair("BUCKET", bucket) + + region := backendS3["region"].(string) + tpl.AddPair("REGION", region) + + s3BackendType, ok := backendS3["type"] + if ok { + switch s3BackendType.(string) { + case "aws": + // AWS, SIGNATURE_VERSION, FORCE_PATH_STYLE are left blank + case "ceph": + tpl.AddPair("SIGNATURE_VERSION", "s3") + tpl.AddPair("FORCE_PATH_STYLE", "YES") + tpl.AddPair("AWS", "no") + default: + tpl.AddPair("AWS", "no") + } + } + + endpoint := backendS3["endpoint_url"].(string) + if len(endpoint) > 0 { + tpl.AddPair("ENDPOINT", endpoint) + } + + totalMB := backendS3["total_size"].(int) + tpl.AddPair("TOTAL_MB", totalMB) + + readLen := backendS3["read_block_length"].(int) + tpl.AddPair("READ_LENGTH", readLen) + + } + + return diags +} + +func generateBackendCommon(backendMap map[string]interface{}, tpl *marketplace.Template) { + + endpoint := backendMap["endpoint_url"].(string) + if len(endpoint) > 0 { + tpl.AddPair("ENDPOINT", endpoint) + } + + rootFSImageSize := backendMap["roofs_image_size"].(int) + if rootFSImageSize > 0 { + tpl.AddPair("IMAGE_SIZE_MB", rootFSImageSize) + } + + filesystem := backendMap["filesystem"].(string) + if len(filesystem) > 0 { + tpl.AddPair("FILESYSTEM", filesystem) + } + + imageBlockFileFormat := backendMap["image_block_file_format"].(string) + if len(imageBlockFileFormat) > 0 { + tpl.AddPair("FORMAT", imageBlockFileFormat) + } + + skipUntested := backendMap["skip_untested"].(bool) + if skipUntested { + tpl.AddPair("SKIP_UNTESTED", skipUntested) + } +} + +func generateBackendLXC(d *schema.ResourceData, meta interface{}, tpl *marketplace.Template) diag.Diagnostics { + var diags diag.Diagnostics + + backendLXCList := d.Get("lxc").(*schema.Set).List() + if len(backendLXCList) > 0 { + + backendLXCIf := backendLXCList[0] + backendLXC := backendLXCIf.(map[string]interface{}) + + tpl.AddPair("MARKET_MAD", LinuxContainers) + + generateBackendCommon(backendLXC, tpl) + + CPU := backendLXC["cpu"].(int) + if CPU > 0 { + tpl.AddPair("CPU", CPU) + } + + vCPU := backendLXC["vcpu"].(int) + if vCPU > 0 { + tpl.AddPair("VCPU", vCPU) + } + + memory := backendLXC["memory"].(int) + if memory > 0 { + tpl.AddPair("MEMORY", memory) + } + + privileged, ok := backendLXC["privileged"] + if ok { + tpl.AddPair("PRIVILEGED", privileged) + } + + } + + return diags +} + +func generateBackendTurnkey(d *schema.ResourceData, meta interface{}, tpl *marketplace.Template) diag.Diagnostics { + var diags diag.Diagnostics + + backendTurnkeyList := d.Get("turnkey").(*schema.Set).List() + if len(backendTurnkeyList) > 0 { + + backendTurnkeyIf := backendTurnkeyList[0] + backendTurnkey := backendTurnkeyIf.(map[string]interface{}) + + tpl.AddPair("MARKET_MAD", TurnkeyLinux) + + generateBackendCommon(backendTurnkey, tpl) + + } + + return diags +} + +func resourceOpennebulaMarketPlaceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + var diags diag.Diagnostics + + mpc, err := getMarketPlaceController(d, meta) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to get the marketplace controller", + Detail: err.Error(), + }) + return diags + } + + marketplaceInfos, err := mpc.Info(false) + if err != nil { + if NoExists(err) { + log.Printf("[WARN] Removing marketplace %s from state because it no longer exists in", d.Get("name")) + d.SetId("") + return nil + } + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed retrieve marketplace informations", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + d.Set("name", marketplaceInfos.Name) + d.Set("uid", marketplaceInfos.UID) + d.Set("gid", marketplaceInfos.GID) + d.Set("uname", marketplaceInfos.UName) + d.Set("gname", marketplaceInfos.GName) + d.Set("permissions", permissionsUnixString(*marketplaceInfos.Permissions)) + + description, err := marketplaceInfos.Template.GetStr("DESCRIPTION") + if err == nil { + d.Set("description", description) + } + + switch marketplaceInfos.MarketMad { + case One: + endpointUrl, _ := marketplaceInfos.Template.GetStr("ENDPOINT") + d.Set("one", []map[string]interface{}{ + { + "endpoint_url": endpointUrl, + }, + }) + case Http: + publicDir, _ := marketplaceInfos.Template.GetStr("PUBLIC_DIR") + baseUrl, _ := marketplaceInfos.Template.GetStr("BASE_URL") + + backendHttp := map[string]interface{}{ + "endpoint_url": baseUrl, + "path": publicDir, + } + + bridgeListStr, err := marketplaceInfos.Template.GetStr("BRIDGE_LIST") + if err == nil { + bridgeList := strings.Split(bridgeListStr, " ") + backendHttp["storage_bridge_list"] = bridgeList + } + + d.Set("http", []map[string]interface{}{backendHttp}) + case S3: + + accessKeyID, _ := marketplaceInfos.Template.GetStr("ACCESS_KEY_ID") + secretAccessKey, _ := marketplaceInfos.Template.GetStr("SECRET_ACCESS_KEY") + bucket, _ := marketplaceInfos.Template.GetStr("BUCKET") + region, _ := marketplaceInfos.Template.GetStr("REGION") + + backendS3 := map[string]interface{}{ + "access_key_id": accessKeyID, + "secret_access_key": secretAccessKey, + "bucket": bucket, + "region": region, + } + + endpoint, err := marketplaceInfos.Template.GetStr("ENDPOINT") + if err == nil { + backendS3["endpoint_url"] = endpoint + } + + totalMB, err := marketplaceInfos.Template.GetI("TOTAL_MB") + if err == nil { + backendS3["total_size"] = totalMB + } + + readLength, err := marketplaceInfos.Template.GetI("READ_LENGTH") + if err == nil { + backendS3["read_block_length"] = readLength + } + + aws, err := marketplaceInfos.Template.GetStr("AWS") + if err != nil || len(aws) == 0 { + // no tags or empty type means AWS type + backendS3["type"] = "aws" + } else { + sigVersion, err := marketplaceInfos.Template.GetStr("SIGNATURE_VERSION") + if err == nil && sigVersion == "s3" { + backendS3["type"] = "ceph" + } else { + backendS3["type"] = "minio" + } + } + + d.Set("s3", []map[string]interface{}{backendS3}) + + case LinuxContainers: + backendLXC := flattenCommonBackend(d, meta, &marketplaceInfos.Template) + + cpu, err := marketplaceInfos.Template.GetInt("CPU") + if err == nil { + backendLXC["cpu"] = cpu + } + + vcpu, err := marketplaceInfos.Template.GetInt("VCPU") + if err == nil { + backendLXC["vcpu"] = vcpu + } + + mem, err := marketplaceInfos.Template.GetInt("MEMORY") + if err == nil { + backendLXC["memory"] = mem + } + + privileged, err := marketplaceInfos.Template.GetStr("PRIVILEGED") + if err == nil { + backendLXC["privileged"] = privileged + } + d.Set("lxc", []map[string]interface{}{backendLXC}) + case TurnkeyLinux: + + backendTurnkey := flattenCommonBackend(d, meta, &marketplaceInfos.Template) + + d.Set("turnkey", []map[string]interface{}{backendTurnkey}) + + case DockerHub: + d.Set("dockerhub", true) + default: + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "The marketplace backend type is not handled by the provider", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + flattenDiags := flattenMarketplaceTemplate(d, meta, &marketplaceInfos.Template) + for _, diag := range flattenDiags { + diags = append(diags, diag) + } + + state, _ := marketplaceInfos.StateString() + d.Set("disabled", state == marketplace.Disabled.String()) + + return diags +} + +// Common schema to linux containers LXC and Turnkey linux +func flattenCommonBackend(d *schema.ResourceData, meta interface{}, marketplaceTpl *marketplace.Template) map[string]interface{} { + backend := make(map[string]interface{}) + + endpoint, err := marketplaceTpl.GetStr("ENDPOINT") + if err == nil { + backend["endpoint_url"] = endpoint + } + + imageSizeMB, err := marketplaceTpl.GetInt("IMAGE_SIZE_MB") + if err == nil { + backend["roofs_image_size"] = imageSizeMB + } + + filesystem, err := marketplaceTpl.GetStr("FILESYSTEM") + if err == nil { + backend["filesystem"] = filesystem + } + + format, err := marketplaceTpl.GetStr("FORMAT") + if err == nil { + backend["image_block_file_format"] = format + } + + skipUntested, err := marketplaceTpl.GetStr("SKIP_UNTESTED") + if err == nil { + backend["skip_untested"] = skipUntested + } + + return backend +} + +func flattenMarketplaceTemplate(d *schema.ResourceData, meta interface{}, marketplaceTpl *marketplace.Template) diag.Diagnostics { + + var diags diag.Diagnostics + + err := flattenTemplateSection(d, meta, &marketplaceTpl.Template) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to read template section", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + } + + flattenDiags := flattenTemplateTags(d, meta, &marketplaceTpl.Template) + for _, diag := range flattenDiags { + diag.Detail = fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err) + diags = append(diags, diag) + } + + return diags +} + +func resourceOpennebulaMarketPlaceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + //config := meta.(*Configuration) + //controller := config.Controller + + var diags diag.Diagnostics + + mpc, err := getMarketPlaceController(d, meta) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to get the marketplace controller", + Detail: err.Error(), + }) + return diags + } + + // template management + + marketplaceInfos, err := mpc.Info(false) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve informations", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + if d.HasChange("name") { + newName := d.Get("name").(string) + err := mpc.Rename(newName) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to rename", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + + update := false + newTpl := marketplaceInfos.Template + + if d.HasChange("one") { + newTpl.Del("MARKET_MAD") + newTpl.Del("ENDPOINT") + + diags = generateBackendOne(d, meta, &newTpl) + if len(diags) > 0 { + return diags + } + + update = true + } + + if d.HasChange("http") { + newTpl.Del("MARKET_MAD") + newTpl.Del("BASE_URL") + newTpl.Del("PUBLIC_DIR") + newTpl.Del("BRIDGE_LIST") + + diags = generateBackendHttp(d, meta, &newTpl) + if len(diags) > 0 { + return diags + } + + update = true + } + + if d.HasChange("s3") { + + for _, k := range []string{"MARKET_MAD", "ACCESS_KEY_ID", "SECRET_ACCESS_KEY", + "BUCKET", "REGION", "ENDPOINT", + "SIGNATURE_VERSION", "FORCE_PATH_STYLE", "TOTAL_MB", + "READ_LENGTH", "AWS"} { + newTpl.Del(k) + } + + diags = generateBackendS3(d, meta, &newTpl) + if len(diags) > 0 { + return diags + } + + update = true + } + + if d.HasChange("lxc") { + + for _, k := range []string{"MARKET_MAD", "ENDPOINT", "IMAGE_SIZE_MB", "FILESYSTEM", + "FORMAT", "SKIP_UNTESTED", "CPU", "VCPU", "MEMORY", "PRIVILEGED"} { + newTpl.Del(k) + } + + diags = generateBackendLXC(d, meta, &newTpl) + if len(diags) > 0 { + return diags + } + + update = true + } + + if d.HasChange("template_section") { + + updateTemplateSection(d, &newTpl.Template) + + update = true + } + + if d.HasChange("tags") { + + oldTagsIf, newTagsIf := d.GetChange("tags") + oldTags := oldTagsIf.(map[string]interface{}) + newTags := newTagsIf.(map[string]interface{}) + + // delete tags + for k, _ := range oldTags { + _, ok := newTags[k] + if ok { + continue + } + newTpl.Del(strings.ToUpper(k)) + } + + // add/update tags + for k, v := range newTags { + key := strings.ToUpper(k) + newTpl.Del(key) + newTpl.AddPair(key, v) + } + + update = true + } + + if d.HasChange("tags_all") { + oldTagsAllIf, newTagsAllIf := d.GetChange("tags_all") + oldTagsAll := oldTagsAllIf.(map[string]interface{}) + newTagsAll := newTagsAllIf.(map[string]interface{}) + + tags := d.Get("tags").(map[string]interface{}) + + // delete tags + for k, _ := range oldTagsAll { + _, ok := newTagsAll[k] + if ok { + continue + } + newTpl.Del(strings.ToUpper(k)) + } + + // reapply all default tags that were neither applied nor overriden via tags section + for k, v := range newTagsAll { + _, ok := tags[k] + if ok { + continue + } + + key := strings.ToUpper(k) + newTpl.Del(key) + newTpl.AddPair(key, v) + } + + update = true + } + + if update { + log.Printf("[DEBUG] update marketplace template: %s", newTpl.String()) + err = mpc.Update(newTpl.String(), parameters.Replace) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update marketplace content", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + } + + if d.HasChange("disabled") { + disabled := d.Get("disabled").(bool) + err := mpc.Enable(!disabled) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to enable/disable the marketplace", + Detail: fmt.Sprintf("marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + + // wait on state transition + timeout := d.Timeout(schema.TimeoutUpdate) + + // expected state when disabling + pendingStates := []string{marketplace.Enabled.String()} + targetStates := []string{marketplace.Disabled.String()} + // expected states when enabling + if disabled { + tmp := pendingStates + pendingStates = targetStates + targetStates = tmp + } + + _, err = waitForMarketplaceStates(ctx, mpc, timeout, pendingStates, targetStates) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to wait marketplace to be in %s state", strings.Join(targetStates, ", ")), + Detail: fmt.Sprintf("marketplace marketplace (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + + return resourceOpennebulaMarketPlaceRead(ctx, d, meta) +} + +func resourceOpennebulaMarketPlaceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + var diags diag.Diagnostics + + mpc, err := getMarketPlaceController(d, meta) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to get the marketplace controller", + Detail: err.Error(), + }) + return diags + } + + err = mpc.Delete() + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to delete", + Detail: fmt.Sprintf("marketplace (ID: %d): %s", mpc.ID, err), + }) + return diags + } + + return nil +} + +// waitForMarketStates wait for a an marketplace to reach some expected states +func waitForMarketplaceStates(ctx context.Context, mpc *goca.MarketPlaceController, timeout time.Duration, pending, target []string) (interface{}, error) { + + stateChangeConf := resource.StateChangeConf{ + Pending: pending, + Target: target, + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + Refresh: func() (interface{}, string, error) { + + log.Println("Refreshing marketplace state...") + + marketInfos, err := mpc.Info(false) + if err != nil { + if NoExists(err) { + return marketInfos, "notfound", nil + } + return marketInfos, "", err + } + state, _ := marketInfos.StateString() + + log.Printf("Marketplace (ID:%d, name:%s) is currently in state %s", marketInfos.ID, marketInfos.Name, state) + + return marketInfos, state, nil + }, + } + + return stateChangeConf.WaitForStateContext(ctx) + +} diff --git a/opennebula/resource_opennebula_marketplace_test.go b/opennebula/resource_opennebula_marketplace_test.go new file mode 100644 index 000000000..292d6af97 --- /dev/null +++ b/opennebula/resource_opennebula_marketplace_test.go @@ -0,0 +1,114 @@ +package opennebula + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccMarketplace(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckMarketplaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMarketplaceConfigBasic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_marketplace.example", "name", "testmp"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "permissions", "642"), + resource.TestCheckTypeSetElemNestedAttrs("opennebula_marketplace.example", "s3.*", map[string]string{ + "type": "aws", + "access_key_id": "testkey", + "secret_access_key": "testsecretkey", + "region": "somewhere", + "bucket": "bucket1", + }), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.%", "2"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.env", "prod"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.customer", "test"), + ), + }, + { + Config: testAccMarketplaceConfigUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_marketplace.example", "name", "renamedmp"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "permissions", "642"), + resource.TestCheckTypeSetElemNestedAttrs("opennebula_marketplace.example", "s3.*", map[string]string{ + "type": "aws", + "access_key_id": "testkey", + "secret_access_key": "testsecretkey", + "region": "somewhere", + "bucket": "bucket2", + }), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.%", "3"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.env", "dev"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.customer", "test"), + resource.TestCheckResourceAttr("opennebula_marketplace.example", "tags.version", "2"), + ), + }, + }, + }) +} + +func testAccCheckMarketplaceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Configuration) + controller := config.Controller + + for _, rs := range s.RootModule().Resources { + mpID, _ := strconv.ParseUint(rs.Primary.ID, 10, 0) + mpc := controller.MarketPlace(int(mpID)) + mp, _ := mpc.Info(false) + if mp != nil { + return fmt.Errorf("Expected marketplace %s to have been destroyed", rs.Primary.ID) + } + } + + return nil +} + +var testAccMarketplaceConfigBasic = ` +resource "opennebula_marketplace" "example" { + name = "testmp" + description = "Terraform marketplace" + permissions = "642" + + s3 { + type = "aws" + access_key_id = "testkey" + secret_access_key = "testsecretkey" + region = "somewhere" + bucket = "bucket1" + } + + tags = { + env = "prod" + customer = "test" + } +} +` + +var testAccMarketplaceConfigUpdate = ` +resource "opennebula_marketplace" "example" { + name = "renamedmp" + description = "Terraform marketplace" + permissions = "642" + + s3 { + type = "aws" + access_key_id = "testkey" + secret_access_key = "testsecretkey" + region = "somewhere" + bucket = "bucket2" + } + + tags = { + env = "dev" + customer = "test" + version = "2" + } +} +` diff --git a/website/docs/r/marketplace.html.markdown b/website/docs/r/marketplace.html.markdown new file mode 100644 index 000000000..5f8a9b62d --- /dev/null +++ b/website/docs/r/marketplace.html.markdown @@ -0,0 +1,124 @@ +--- +layout: "opennebula" +page_title: "OpenNebula: opennebula_marketplace" +sidebar_current: "docs-opennebula-resource-marketplace" +description: |- + Provides an OpenNebula marketplace resource. +--- + +# opennebula_marketplace + +Provides an OpenNebula marketplace resource. + +This resource allows you to manage marketplaces. + +## Example Usage + +Create a custom marketplace: + +```hcl +resource "opennebula_marketplace" "example" { + name = "example" + + s3 { + type = "aws" + access_key_id = "XXX" + secret_access_key = "XXX" + region = "us" + bucket = "123" + } + + tags = { + environment = "example" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the marketplace. +* `description` - (Optional) The description of the marketplace. +* `one` - (Optional) See [One](#One) section for details. +* `http` - (Optional) See [Http](#Http) section for details. +* `s3` - (Optional) See [S3](#S3) section for details. +* `lxc` - (Optional) See [LXC](#LXC) section for details. +* `turnkey` - (Optional) See [Turnkey](#Turnkey) section for details. +* `dockerhub` - (Optional) Flag as a dockerhub marketplace, this provide access to DockerHub Official Images. +* `tags` - (Optional) Marketplace tags (Key = value). + +### One + +The OpenNebula Marketplace is a catalog of virtual appliances ready to run in OpenNebula environments. + +The following arguments are supported: + +* `endpoint_url` - (Optional) The endpoint URL of the marketplace. + +### Http + +Http Marketplace uses a conventional HTTP server to expose the images (Marketplace Appliances) uploaded to the Marketplace. + +The following arguments are supported: + +* `endpoint_url` - (Required) Base URL of the Marketplace HTTP endpoint. +* `path` - (Required) Absolute directory path to place images in the front-end or in the hosts pointed at by storage_bridge_list. +* `storage_bridge_list` - (Optional) List of servers to access the public directory. + +### S3 + +This Marketplace uses an S3 API-capable service as the Back-end. + +The following arguments are supported: + +* `type` - (Optional) Type of the s3 backend: aws, ceph, minio. +* `access_key_id` - (Required) The access key of the S3 user. +* `secret_access_key` - (Required) The secret key of the S3 user. +* `bucket` - (Required) The bucket where the files will be stored. +* `region` - (Required) The region to connect to. Any value will work with Ceph-S3. +* `endpoint_url` - (Optional) Only required when connecteing to a service other than Amazon S3. +* `total_size` - (Optional) Define the total size of the marketplace in MB. +* `read_block_length` - (Optional) Split the file into chunks of this size in MB, never user a value larger than 100. + +### LXC + +The Linux Containers image server hosts a public image server with container images for LXC and LXD. + +The following arguments are supported: + +* `endpoint_url` - (Optional) The base URL of the Market. +* `roofs_image_size` - (Optional) Size in MB for the image holding the rootfs. +* `filesystem` - (Optional) Filesystem used for the image. +* `image_block_file_format` - (Optional) Image block file format. +* `skip_untested` - (Optional) Include only apps with support for context. +* `cpu` - (Optional) VM template CPU. +* `vcpu` - (Optional) VM template VCPU. +* `memory` - (Optional) VM template memory. +* `privileged` - (Optional) Secrurity mode of the Linux Container. + +## Turnkey + +The following arguments are supported: + +* `endpoint_url` - (Optional) The base URL of the Market. +* `roofs_image_size` - (Optional) Size in MB for the image holding the rootfs. +* `filesystem` - (Optional) Filesystem used for the image. +* `image_block_file_format` - (Optional) Image block file format. +* `skip_untested` - (Optional) Include only apps with support for context. + +## Attribute Reference + +The following attributes are exported: + +* `id` - ID of the marketplace. +* `tags_all` - Result of the applied `default_tags` and then resource `tags`. +* `default_tags` - Default tags defined in the provider configuration. + +## Import + +`opennebula_marketplace` can be imported using its ID: + +```shell +terraform import opennebula_marketplace.example 123 +```