From c927a8c19f299b04cd622865335800ac251e6b84 Mon Sep 17 00:00:00 2001 From: Shunsuke Suzuki Date: Sat, 9 Jan 2021 00:38:30 +0900 Subject: [PATCH] feat: supports Docker plugin (#35) Closes #24 --- docker/data_source_docker_plugin.go | 80 +++++ docker/data_source_docker_plugin_test.go | 39 +++ docker/provider.go | 2 + docker/resource_docker_container.go | 2 +- docker/resource_docker_container_v1.go | 2 +- docker/resource_docker_plugin.go | 95 ++++++ docker/resource_docker_plugin_funcs.go | 262 ++++++++++++++ docker/resource_docker_plugin_test.go | 414 +++++++++++++++++++++++ docker/resource_docker_service.go | 24 +- go.mod | 2 +- website/docs/d/plugin.html.markdown | 37 ++ website/docs/r/image.html.markdown | 2 +- website/docs/r/plugin.html.markdown | 102 ++++++ 13 files changed, 1047 insertions(+), 16 deletions(-) create mode 100644 docker/data_source_docker_plugin.go create mode 100644 docker/data_source_docker_plugin_test.go create mode 100644 docker/resource_docker_plugin.go create mode 100644 docker/resource_docker_plugin_funcs.go create mode 100644 docker/resource_docker_plugin_test.go create mode 100644 website/docs/d/plugin.html.markdown create mode 100644 website/docs/r/plugin.html.markdown diff --git a/docker/data_source_docker_plugin.go b/docker/data_source_docker_plugin.go new file mode 100644 index 000000000..9bcb0a3fd --- /dev/null +++ b/docker/data_source_docker_plugin.go @@ -0,0 +1,80 @@ +package docker + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceDockerPlugin() *schema.Resource { + return &schema.Resource{ + Read: dataSourceDockerPluginRead, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "alias": { + Type: schema.TypeString, + Optional: true, + Description: "Docker Plugin alias", + }, + + "plugin_reference": { + Type: schema.TypeString, + Description: "Docker Plugin Reference", + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "grant_all_permissions": { + Type: schema.TypeBool, + Computed: true, + Description: "If true, grant all permissions necessary to run the plugin", + }, + "env": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +var errDataSourceKeyIsMissing = errors.New("One of id or alias must be assigned") + +func getDataSourcePluginKey(d *schema.ResourceData) (string, error) { + id, idOK := d.GetOk("id") + alias, aliasOK := d.GetOk("alias") + if idOK { + if aliasOK { + return "", errDataSourceKeyIsMissing + } + return id.(string), nil + } + if aliasOK { + return alias.(string), nil + } + return "", errDataSourceKeyIsMissing +} + +func dataSourceDockerPluginRead(d *schema.ResourceData, meta interface{}) error { + key, err := getDataSourcePluginKey(d) + if err != nil { + return err + } + client := meta.(*ProviderConfig).DockerClient + ctx := context.Background() + plugin, _, err := client.PluginInspectWithRaw(ctx, key) + if err != nil { + return fmt.Errorf("inspect a Docker plugin "+key+": %w", err) + } + + setDockerPlugin(d, plugin) + return nil +} diff --git a/docker/data_source_docker_plugin_test.go b/docker/data_source_docker_plugin_test.go new file mode 100644 index 000000000..f9a13d1a8 --- /dev/null +++ b/docker/data_source_docker_plugin_test.go @@ -0,0 +1,39 @@ +package docker + +import ( + "os/exec" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccDockerPluginDataSource_basic(t *testing.T) { + pluginName := "tiborvass/sample-volume-plugin" + // This fails if the plugin is already installed. + if err := exec.Command("docker", "plugin", "install", pluginName).Run(); err != nil { + t.Fatal(err) + } + defer func() { + if err := exec.Command("docker", "plugin", "rm", "-f", pluginName).Run(); err != nil { + t.Logf("failed to remove the Docker plugin %s: %v", pluginName, err) + } + }() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDockerPluginDataSourceTest, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.docker_plugin.test", "plugin_reference", "docker.io/tiborvass/sample-volume-plugin:latest"), + ), + }, + }, + }) +} + +const testAccDockerPluginDataSourceTest = ` +data "docker_plugin" "test" { + alias = "tiborvass/sample-volume-plugin:latest" +} +` diff --git a/docker/provider.go b/docker/provider.go index 2421f04a5..4d52fd700 100644 --- a/docker/provider.go +++ b/docker/provider.go @@ -109,11 +109,13 @@ func Provider() terraform.ResourceProvider { "docker_config": resourceDockerConfig(), "docker_secret": resourceDockerSecret(), "docker_service": resourceDockerService(), + "docker_plugin": resourceDockerPlugin(), }, DataSourcesMap: map[string]*schema.Resource{ "docker_registry_image": dataSourceDockerRegistryImage(), "docker_network": dataSourceDockerNetwork(), + "docker_plugin": dataSourceDockerPlugin(), }, ConfigureFunc: providerConfigure, diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 7f0531ac3..154206dc5 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -310,7 +310,7 @@ func resourceDockerContainer() *schema.Resource { }, "driver_name": { Type: schema.TypeString, - Description: "Name of the driver to use to create the volume.", + Description: "Name of the driver to use to create the volume", Optional: true, }, "driver_options": { diff --git a/docker/resource_docker_container_v1.go b/docker/resource_docker_container_v1.go index 9372b6be6..fa7234835 100644 --- a/docker/resource_docker_container_v1.go +++ b/docker/resource_docker_container_v1.go @@ -275,7 +275,7 @@ func resourceDockerContainerV1() *schema.Resource { }, "driver_name": { Type: schema.TypeString, - Description: "Name of the driver to use to create the volume.", + Description: "Name of the driver to use to create the volume", Optional: true, }, "driver_options": { diff --git a/docker/resource_docker_plugin.go b/docker/resource_docker_plugin.go new file mode 100644 index 000000000..e4207374c --- /dev/null +++ b/docker/resource_docker_plugin.go @@ -0,0 +1,95 @@ +package docker + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceDockerPlugin() *schema.Resource { + return &schema.Resource{ + Create: resourceDockerPluginCreate, + Read: resourceDockerPluginRead, + Update: resourceDockerPluginUpdate, + Delete: resourceDockerPluginDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Docker Plugin name", + DiffSuppressFunc: diffSuppressFuncPluginName, + ValidateFunc: validateFuncPluginName, + }, + "alias": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + Description: "Docker Plugin alias", + DiffSuppressFunc: func(k, oldV, newV string, d *schema.ResourceData) bool { + return complementTag(oldV) == complementTag(newV) + }, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "grant_all_permissions": { + Type: schema.TypeBool, + Optional: true, + Description: "If true, grant all permissions necessary to run the plugin", + ConflictsWith: []string{"grant_permissions"}, + }, + "grant_permissions": { + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"grant_all_permissions"}, + Set: dockerPluginGrantPermissionsSetFunc, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "env": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "plugin_reference": { + Type: schema.TypeString, + Description: "Docker Plugin Reference", + Computed: true, + }, + + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + }, + "enable_timeout": { + Type: schema.TypeInt, + Optional: true, + Description: "HTTP client timeout to enable the plugin", + }, + "force_disable": { + Type: schema.TypeBool, + Optional: true, + Description: "If true, then the plugin is disabled forcibly when the plugin is disabled", + }, + }, + } +} diff --git a/docker/resource_docker_plugin_funcs.go b/docker/resource_docker_plugin_funcs.go new file mode 100644 index 000000000..6b15efe9f --- /dev/null +++ b/docker/resource_docker_plugin_funcs.go @@ -0,0 +1,262 @@ +package docker + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "strings" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func getDockerPluginEnv(src interface{}) []string { + if src == nil { + return nil + } + b := src.(*schema.Set) + envs := make([]string, b.Len()) + for i, a := range b.List() { + envs[i] = a.(string) + } + return envs +} + +func dockerPluginGrantPermissionsSetFunc(v interface{}) int { + return schema.HashString(v.(map[string]interface{})["name"].(string)) +} + +func complementTag(image string) string { + if strings.Contains(image, ":") { + return image + } + return image + ":latest" +} + +func normalizePluginName(name string) (string, error) { + ref, err := reference.ParseAnyReference(name) + if err != nil { + return "", fmt.Errorf("parse the plugin name: %w", err) + } + return complementTag(ref.String()), nil +} + +func diffSuppressFuncPluginName(k, oldV, newV string, d *schema.ResourceData) bool { + o, err := normalizePluginName(oldV) + if err != nil { + return false + } + n, err := normalizePluginName(newV) + if err != nil { + return false + } + return o == n +} + +func validateFuncPluginName(val interface{}, key string) (warns []string, errs []error) { + if _, err := normalizePluginName(val.(string)); err != nil { + return warns, append(errs, fmt.Errorf("%s is invalid: %w", key, err)) + } + return +} + +func getDockerPluginGrantPermissions(src interface{}) func(types.PluginPrivileges) (bool, error) { + grantPermissionsSet := src.(*schema.Set) + grantPermissions := make(map[string]map[string]struct{}, grantPermissionsSet.Len()) + for _, b := range grantPermissionsSet.List() { + c := b.(map[string]interface{}) + name := c["name"].(string) + values := c["value"].(*schema.Set) + grantPermission := make(map[string]struct{}, values.Len()) + for _, value := range values.List() { + grantPermission[value.(string)] = struct{}{} + } + grantPermissions[name] = grantPermission + } + return func(privileges types.PluginPrivileges) (bool, error) { + for _, privilege := range privileges { + grantPermission, nameOK := grantPermissions[privilege.Name] + if !nameOK { + log.Print("[DEBUG] to install the plugin, the following permissions are required: " + privilege.Name) + return false, nil + } + for _, value := range privilege.Value { + if _, ok := grantPermission[value]; !ok { + log.Print("[DEBUG] to install the plugin, the following permissions are required: " + privilege.Name + " " + value) + return false, nil + } + } + } + return true, nil + } +} + +func resourceDockerPluginCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ProviderConfig).DockerClient + ctx := context.Background() + pluginName := d.Get("name").(string) + alias := d.Get("alias").(string) + log.Printf("[DEBUG] Install a Docker plugin " + pluginName) + opts := types.PluginInstallOptions{ + RemoteRef: pluginName, + AcceptAllPermissions: d.Get("grant_all_permissions").(bool), + Disabled: !d.Get("enabled").(bool), + // TODO support other settings + Args: getDockerPluginEnv(d.Get("env")), + } + if v, ok := d.GetOk("grant_permissions"); ok { + opts.AcceptPermissionsFunc = getDockerPluginGrantPermissions(v) + } + body, err := client.PluginInstall(ctx, alias, opts) + if err != nil { + return fmt.Errorf("install a Docker plugin "+pluginName+": %w", err) + } + _, _ = ioutil.ReadAll(body) + key := pluginName + if alias != "" { + key = alias + } + plugin, _, err := client.PluginInspectWithRaw(ctx, key) + if err != nil { + return fmt.Errorf("inspect a Docker plugin "+key+": %w", err) + } + setDockerPlugin(d, plugin) + return nil +} + +func setDockerPlugin(d *schema.ResourceData, plugin *types.Plugin) { + d.SetId(plugin.ID) + d.Set("plugin_reference", plugin.PluginReference) + d.Set("alias", plugin.Name) + d.Set("name", plugin.PluginReference) + d.Set("enabled", plugin.Enabled) + // TODO support other settings + // https://docs.docker.com/engine/reference/commandline/plugin_set/#extended-description + // source of mounts .Settings.Mounts + // path of devices .Settings.Devices + // args .Settings.Args + d.Set("env", plugin.Settings.Env) +} + +func resourceDockerPluginRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ProviderConfig).DockerClient + ctx := context.Background() + pluginID := d.Id() + plugin, _, err := client.PluginInspectWithRaw(ctx, pluginID) + if err != nil { + log.Printf("[DEBUG] Inspect a Docker plugin "+pluginID+": %w", err) + d.SetId("") + return nil + } + setDockerPlugin(d, plugin) + return nil +} + +func disablePlugin(ctx context.Context, d *schema.ResourceData, cl *client.Client) error { + pluginID := d.Id() + log.Printf("[DEBUG] Disable a Docker plugin " + pluginID) + if err := cl.PluginDisable(ctx, pluginID, types.PluginDisableOptions{ + Force: d.Get("force_disable").(bool), + }); err != nil { + return fmt.Errorf("disable the Docker plugin "+pluginID+": %w", err) + } + return nil +} + +func enablePlugin(ctx context.Context, d *schema.ResourceData, cl *client.Client) error { + pluginID := d.Id() + log.Print("[DEBUG] Enable a Docker plugin " + pluginID) + if err := cl.PluginEnable(ctx, pluginID, types.PluginEnableOptions{ + Timeout: d.Get("enable_timeout").(int), + }); err != nil { + return fmt.Errorf("enable the Docker plugin "+pluginID+": %w", err) + } + return nil +} + +func pluginSet(ctx context.Context, d *schema.ResourceData, cl *client.Client) error { + pluginID := d.Id() + log.Printf("[DEBUG] Update settings of a Docker plugin " + pluginID) + // currently, only environment variables are supported. + // TODO support other args + // https://docs.docker.com/engine/reference/commandline/plugin_set/#extended-description + // source of mounts .Settings.Mounts + // path of devices .Settings.Devices + // args .Settings.Args + if err := cl.PluginSet(ctx, pluginID, getDockerPluginEnv(d.Get("env"))); err != nil { + return fmt.Errorf("modifiy settings for the Docker plugin "+pluginID+": %w", err) + } + return nil +} + +func pluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) (gErr error) { + cl := meta.(*ProviderConfig).DockerClient + o, n := d.GetChange("enabled") + oldEnabled, newEnabled := o.(bool), n.(bool) + if d.HasChange("env") { + if oldEnabled { + // To update the plugin setttings, the plugin must be disabled + if err := disablePlugin(ctx, d, cl); err != nil { + return err + } + if newEnabled { + defer func() { + if err := enablePlugin(ctx, d, cl); err != nil { + if gErr == nil { + gErr = err + return + } + } + }() + } + } + if err := pluginSet(ctx, d, cl); err != nil { + return err + } + if !oldEnabled && newEnabled { + if err := enablePlugin(ctx, d, cl); err != nil { + return err + } + } + return nil + } + // update only "enabled" + if d.HasChange("enabled") { + if newEnabled { + if err := enablePlugin(ctx, d, cl); err != nil { + return err + } + } else { + if err := disablePlugin(ctx, d, cl); err != nil { + return err + } + } + } + return nil +} + +func resourceDockerPluginUpdate(d *schema.ResourceData, meta interface{}) error { + ctx := context.Background() + if err := pluginUpdate(ctx, d, meta); err != nil { + return err + } + // call the read function to update the resource's state. + // https://learn.hashicorp.com/tutorials/terraform/provider-update?in=terraform/providers#implement-update + return resourceDockerPluginRead(d, meta) +} + +func resourceDockerPluginDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ProviderConfig).DockerClient + ctx := context.Background() + pluginID := d.Id() + log.Printf("[DEBUG] Remove a Docker plugin " + pluginID) + if err := client.PluginRemove(ctx, pluginID, types.PluginRemoveOptions{ + Force: d.Get("force_destroy").(bool), + }); err != nil { + return fmt.Errorf("remove the Docker plugin "+pluginID+": %w", err) + } + return nil +} diff --git a/docker/resource_docker_plugin_test.go b/docker/resource_docker_plugin_test.go new file mode 100644 index 000000000..8ad0f9d49 --- /dev/null +++ b/docker/resource_docker_plugin_test.go @@ -0,0 +1,414 @@ +package docker + +import ( + "reflect" + "testing" + + "github.com/docker/docker/api/types" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func Test_getDockerPluginEnv(t *testing.T) { + t.Parallel() + data := []struct { + title string + src interface{} + exp []string + }{ + { + title: "nil", + }, + { + title: "basic", + src: schema.NewSet(schema.HashString, []interface{}{"DEBUG=1"}), + exp: []string{"DEBUG=1"}, + }, + } + for _, d := range data { + d := d + t.Run(d.title, func(t *testing.T) { + t.Parallel() + envs := getDockerPluginEnv(d.src) + if !reflect.DeepEqual(d.exp, envs) { + t.Fatalf("want %v, got %v", d.exp, envs) + } + }) + } +} + +func Test_complementTag(t *testing.T) { + t.Parallel() + data := []struct { + title string + image string + exp string + }{ + { + title: "alpine:3.11.6", + image: "alpine:3.11.6", + exp: "alpine:3.11.6", + }, + { + title: "alpine", + image: "alpine", + exp: "alpine:latest", + }, + } + for _, d := range data { + d := d + t.Run(d.title, func(t *testing.T) { + t.Parallel() + image := complementTag(d.image) + if image != d.exp { + t.Fatalf("want %v, got %v", d.exp, image) + } + }) + } +} + +func Test_normalizePluginName(t *testing.T) { + t.Parallel() + data := []struct { + title string + image string + isErr bool + exp string + }{ + { + title: "alpine:3.11.6", + image: "alpine:3.11.6", + exp: "docker.io/library/alpine:3.11.6", + }, + { + title: "alpine", + image: "alpine", + exp: "docker.io/library/alpine:latest", + }, + { + title: "vieux/sshfs", + image: "vieux/sshfs", + exp: "docker.io/vieux/sshfs:latest", + }, + { + title: "docker.io/vieux/sshfs:latest", + image: "docker.io/vieux/sshfs:latest", + exp: "docker.io/vieux/sshfs:latest", + }, + { + title: "docker.io/vieux/sshfs", + image: "docker.io/vieux/sshfs", + exp: "docker.io/vieux/sshfs:latest", + }, + } + for _, d := range data { + d := d + t.Run(d.title, func(t *testing.T) { + t.Parallel() + image, err := normalizePluginName(d.image) + if d.isErr { + if err == nil { + t.Fatal("error should be returned") + } + return + } + if err != nil { + t.Fatal(err) + } + if image != d.exp { + t.Fatalf("want %v, got %v", d.exp, image) + } + }) + } +} + +func Test_getDockerPluginGrantPermissions(t *testing.T) { + t.Parallel() + data := []struct { + title string + src interface{} + privileges types.PluginPrivileges + exp bool + isErr bool + }{ + { + title: "no privilege", + src: schema.NewSet(dockerPluginGrantPermissionsSetFunc, []interface{}{ + map[string]interface{}{ + "name": "network", + "value": schema.NewSet(schema.HashString, []interface{}{"host"}), + }, + }), + exp: true, + }, + { + title: "basic", + src: schema.NewSet(dockerPluginGrantPermissionsSetFunc, []interface{}{ + map[string]interface{}{ + "name": "network", + "value": schema.NewSet(schema.HashString, []interface{}{"host"}), + }, + }), + privileges: types.PluginPrivileges{ + { + Name: "network", + Value: []string{"host"}, + }, + }, + exp: true, + }, + { + title: "permission denied 1", + src: schema.NewSet(dockerPluginGrantPermissionsSetFunc, []interface{}{ + map[string]interface{}{ + "name": "network", + "value": schema.NewSet(schema.HashString, []interface{}{ + "host", + }), + }, + }), + privileges: types.PluginPrivileges{ + { + Name: "device", + Value: []string{"/dev/fuse"}, + }, + }, + exp: false, + }, + { + title: "permission denied 2", + src: schema.NewSet(dockerPluginGrantPermissionsSetFunc, []interface{}{ + map[string]interface{}{ + "name": "network", + "value": schema.NewSet(schema.HashString, []interface{}{ + "host", + }), + }, + map[string]interface{}{ + "name": "mount", + "value": schema.NewSet(schema.HashString, []interface{}{ + "/var/lib/docker/plugins/", + }), + }, + }), + privileges: types.PluginPrivileges{ + { + Name: "network", + Value: []string{"host"}, + }, + { + Name: "mount", + Value: []string{"", "/var/lib/docker/plugins/"}, + }, + }, + exp: false, + }, + } + for _, d := range data { + d := d + t.Run(d.title, func(t *testing.T) { + t.Parallel() + f := getDockerPluginGrantPermissions(d.src) + b, err := f(d.privileges) + if d.isErr { + if err == nil { + t.Fatal("error must be returned") + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(d.exp, b) { + t.Fatalf("want %v, got %v", d.exp, b) + } + }) + } +} + +func TestAccDockerPlugin_basic(t *testing.T) { + const resourceName = "docker_plugin.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + ResourceName: resourceName, + Config: testAccDockerPluginMinimum, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + ), + }, + { + ResourceName: resourceName, + Config: testAccDockerPluginAlias, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "sample:latest"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + ), + }, + { + ResourceName: resourceName, + Config: testAccDockerPluginDisableWhenSet, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "sample:latest"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "grant_all_permissions", "true"), + resource.TestCheckResourceAttr(resourceName, "force_destroy", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_timeout", "60"), + ), + }, + { + ResourceName: resourceName, + Config: testAccDockerPluginDisabled, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/tiborvass/sample-volume-plugin:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "sample:latest"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "grant_all_permissions", "true"), + resource.TestCheckResourceAttr(resourceName, "force_destroy", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_timeout", "60"), + resource.TestCheckResourceAttr(resourceName, "force_disable", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +func TestAccDockerPlugin_grantAllPermissions(t *testing.T) { + const resourceName = "docker_plugin.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + ResourceName: resourceName, + Config: testAccDockerPluginGrantAllPermissions, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/vieux/sshfs:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/vieux/sshfs:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "vieux/sshfs:latest"), + resource.TestCheckResourceAttr(resourceName, "grant_all_permissions", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +func TestAccDockerPlugin_grantPermissions(t *testing.T) { + const resourceName = "docker_plugin.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + ResourceName: resourceName, + Config: testAccDockerPluginGrantPermissions, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "docker.io/vieux/sshfs:latest"), + resource.TestCheckResourceAttr(resourceName, "plugin_reference", "docker.io/vieux/sshfs:latest"), + resource.TestCheckResourceAttr(resourceName, "alias", "vieux/sshfs:latest"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +const testAccDockerPluginMinimum = ` +resource "docker_plugin" "test" { + name = "docker.io/tiborvass/sample-volume-plugin:latest" + force_destroy = true +}` + +const testAccDockerPluginAlias = ` +resource "docker_plugin" "test" { + name = "docker.io/tiborvass/sample-volume-plugin:latest" + alias = "sample:latest" + force_destroy = true +}` + +const testAccDockerPluginDisableWhenSet = ` +resource "docker_plugin" "test" { + name = "docker.io/tiborvass/sample-volume-plugin:latest" + alias = "sample:latest" + grant_all_permissions = true + force_destroy = true + enable_timeout = 60 + env = [ + "DEBUG=1" + ] +}` + +const testAccDockerPluginDisabled = ` +resource "docker_plugin" "test" { + name = "docker.io/tiborvass/sample-volume-plugin:latest" + alias = "sample:latest" + enabled = false + grant_all_permissions = true + force_destroy = true + force_disable = true + enable_timeout = 60 + env = [ + "DEBUG=1" + ] +}` + +// To install this plugin, it is required to grant required permissions. +const testAccDockerPluginGrantAllPermissions = ` +resource "docker_plugin" "test" { + name = "docker.io/vieux/sshfs:latest" + grant_all_permissions = true + force_destroy = true +}` + +// To install this plugin, it is required to grant required permissions. +const testAccDockerPluginGrantPermissions = ` +resource "docker_plugin" "test" { + name = "vieux/sshfs" + force_destroy = true + grant_permissions { + name = "network" + value = [ + "host" + ] + } + grant_permissions { + name = "mount" + value = [ + "", + "/var/lib/docker/plugins/" + ] + } + grant_permissions { + name = "device" + value = [ + "/dev/fuse" + ] + } + grant_permissions { + name = "capabilities" + value = [ + "CAP_SYS_ADMIN" + ] + } +}` diff --git a/docker/resource_docker_service.go b/docker/resource_docker_service.go index ba954b3dd..1f860a6f6 100644 --- a/docker/resource_docker_service.go +++ b/docker/resource_docker_service.go @@ -260,7 +260,7 @@ func resourceDockerService() *schema.Resource { }, "driver_name": { Type: schema.TypeString, - Description: "Name of the driver to use to create the volume.", + Description: "Name of the driver to use to create the volume", Optional: true, }, "driver_options": { @@ -354,7 +354,7 @@ func resourceDockerService() *schema.Resource { }, "hosts": { Type: schema.TypeSet, - Description: "A list of hostname/IP mappings to add to the container's hosts file.", + Description: "A list of hostname/IP mappings to add to the container's hosts file", Optional: true, ForceNew: true, Elem: &schema.Resource{ @@ -597,7 +597,7 @@ func resourceDockerService() *schema.Resource { }, "restart_policy": { Type: schema.TypeMap, - Description: "Specification for the restart policy which applies to containers created as part of this service.", + Description: "Specification for the restart policy which applies to containers created as part of this service", Optional: true, Computed: true, Elem: &schema.Resource{ @@ -695,7 +695,7 @@ func resourceDockerService() *schema.Resource { }, "networks": { Type: schema.TypeSet, - Description: "Ids of the networks in which the container will be put in.", + Description: "Ids of the networks in which the container will be put in", Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, @@ -883,7 +883,7 @@ func resourceDockerService() *schema.Resource { }, "ports": { Type: schema.TypeList, - Description: "List of exposed ports that this service is accessible on from the outside. Ports can only be provided if 'vip' resolution mode is used.", + Description: "List of exposed ports that this service is accessible on from the outside. Ports can only be provided if 'vip' resolution mode is used", Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -906,7 +906,7 @@ func resourceDockerService() *schema.Resource { }, "published_port": { Type: schema.TypeInt, - Description: "The port on the swarm hosts.", + Description: "The port on the swarm hosts", Optional: true, Computed: true, }, @@ -1205,7 +1205,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "driver_name": { Type: schema.TypeString, - Description: "Name of the driver to use to create the volume.", + Description: "Name of the driver to use to create the volume", Optional: true, }, "driver_options": { @@ -1299,7 +1299,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "hosts": { Type: schema.TypeSet, - Description: "A list of hostname/IP mappings to add to the container's hosts file.", + Description: "A list of hostname/IP mappings to add to the container's hosts file", Optional: true, ForceNew: true, Elem: &schema.Resource{ @@ -1504,7 +1504,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "restart_policy": { Type: schema.TypeMap, - Description: "Specification for the restart policy which applies to containers created as part of this service.", + Description: "Specification for the restart policy which applies to containers created as part of this service", Optional: true, Computed: true, Elem: &schema.Resource{ @@ -1596,7 +1596,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "networks": { Type: schema.TypeSet, - Description: "Ids of the networks in which the container will be put in.", + Description: "Ids of the networks in which the container will be put in", Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, @@ -1784,7 +1784,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "ports": { Type: schema.TypeSet, - Description: "List of exposed ports that this service is accessible on from the outside. Ports can only be provided if 'vip' resolution mode is used.", + Description: "List of exposed ports that this service is accessible on from the outside. Ports can only be provided if 'vip' resolution mode is used", Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -1807,7 +1807,7 @@ func resourceDockerServiceV0() *schema.Resource { }, "published_port": { Type: schema.TypeInt, - Description: "The port on the swarm hosts.", + Description: "The port on the swarm hosts", Optional: true, }, "publish_mode": { diff --git a/go.mod b/go.mod index ade4195fe..07b48e294 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/Microsoft/hcsshim v0.8.9 // indirect github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect github.com/docker/cli v0.0.0-20200303215952-eb310fca4956 // v19.03.8 - github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478 // indirect + github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478 github.com/docker/docker v0.7.3-0.20190525203055-f25e0c6f3093 github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/go-connections v0.4.0 diff --git a/website/docs/d/plugin.html.markdown b/website/docs/d/plugin.html.markdown new file mode 100644 index 000000000..2fa731a0c --- /dev/null +++ b/website/docs/d/plugin.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "docker" +page_title: "Docker: docker_plugin" +sidebar_current: "docs-docker-datasource-plugin" +description: |- + Reads the local Docker pluign. +--- + +# docker\_plugin + +Reads the local Docker plugin. The plugin must be installed locally. + +## Example Usage + +```hcl +data "docker_plugin" "sample-volume-plugin" { + alias = "sample-volume-plugin:latest" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Optional, string) The Docker plugin ID. +* `alias` - (Optional, string) The alias of the Docker plugin. + +One of `id` or `alias` must be assigned. + +## Attributes Reference + +The following attributes are exported in addition to the above configuration: + +* `plugin_reference` - (Optional, string, Forces new resource) The plugin reference. +* `disabled` - (Optional, boolean) If true, the plugin is disabled. +* `grant_all_permissions` - (Optional, boolean) If true, grant all permissions necessary to run the plugin. +* `args` - (Optional, set of string). Currently, only environment variables are supported. diff --git a/website/docs/r/image.html.markdown b/website/docs/r/image.html.markdown index 6dcfcc330..8b0538f6c 100644 --- a/website/docs/r/image.html.markdown +++ b/website/docs/r/image.html.markdown @@ -52,7 +52,7 @@ The following arguments are supported: registry when using the `docker_registry_image` [data source](/docs/providers/docker/d/registry_image.html) to trigger an image update. * `pull_trigger` - **Deprecated**, use `pull_triggers` instead. -* `force_remove` - (Optional, boolean) If true, then the image is removed Forcely when the resource is destroyed. +* `force_remove` - (Optional, boolean) If true, then the image is removed forcibly when the resource is destroyed. * `build` - (Optional, block) See [Build](#build-1) below for details. diff --git a/website/docs/r/plugin.html.markdown b/website/docs/r/plugin.html.markdown new file mode 100644 index 000000000..8cd4489b0 --- /dev/null +++ b/website/docs/r/plugin.html.markdown @@ -0,0 +1,102 @@ +--- +layout: "docker" +page_title: "Docker: docker_plugin" +sidebar_current: "docs-docker-resource-plugin" +description: |- + Manages the lifecycle of a Docker plugin. +--- + +# docker\_plugin + +Manages the lifecycle of a Docker plugin. + +## Example Usage + +```hcl +resource "docker_plugin" "sample-volume-plugin" { + name = "docker.io/tiborvass/sample-volume-plugin:latest" +} +``` + +```hcl +resource "docker_plugin" "sample-volume-plugin" { + name = "tiborvass/sample-volume-plugin" + alias = "sample-volume-plugin" + enabled = false + grant_all_permissions = true + force_destroy = true + enable_timeout = 60 + force_disable = true + env = [ + "DEBUG=1" + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required, string, Forces new resource) The plugin name. If the tag is omitted, `:latest` is complemented to the attribute value. +* `alias` - (Optional, string, Forces new resource) The alias of the Docker plugin. If the tag is omitted, `:latest` is complemented to the attribute value. +* `enabled` - (Optional, boolean) If true, the plugin is enabled. The default value is `true`. +* `grant_all_permissions` - (Optional, boolean) If true, grant all permissions necessary to run the plugin. This attribute conflicts with `grant_permissions`. +* `grant_permissions` - (Optional, block) grant permissions necessary to run the plugin. This attribute conflicts with `grant_all_permissions`. See [grant_permissions](#grant-permissions-1) below for details. +* `env` - (Optional, set of string). The environment variables. +* `force_destroy` - (Optional, boolean) If true, the plugin is removed forcibly when the plugin is removed. +* `enable_timeout` - (Optional, int) HTTP client timeout to enable the plugin. +* `force_disable` - (Optional, boolean) If true, then the plugin is disabled forcibly when the plugin is disabled. + + +## grant_permissions + +`grant_permissions` is a block within the configuration that can be repeated to grant permissions to install the plugin. Each `grant_permissions` block supports +the following: + +* `name` - (Required, string) +* `value` - (Required, list of string) + +Example: + +```hcl +resource "docker_plugin" "sshfs" { + name = "docker.io/vieux/sshfs:latest" + grant_permissions { + name = "network" + value = [ + "host" + ] + } + grant_permissions { + name = "mount" + value = [ + "", + "/var/lib/docker/plugins/" + ] + } + grant_permissions { + name = "device" + value = [ + "/dev/fuse" + ] + } + grant_permissions { + name = "capabilities" + value = [ + "CAP_SYS_ADMIN" + ] + } +} +``` + +## Attributes Reference + +* `plugin_reference` - (string) The plugin reference. + +## Import + +Docker plugins can be imported using the long id, e.g. for a plugin `tiborvass/sample-volume-plugin:latest`: + +```sh +$ terraform import docker_plugin.sample-volume-plugin $(docker plugin inspect -f "{{.ID}}" tiborvass/sample-volume-plugin:latest) +```