diff --git a/docs/resources/container.md b/docs/resources/container.md new file mode 100644 index 00000000..382aac9d --- /dev/null +++ b/docs/resources/container.md @@ -0,0 +1,52 @@ +# routeros_container (Resource) + + + + + +## Schema + +### Required + +- `interface` (String) veth interface to be used with the container + +### Optional + +- `___id___` (Number) Resource ID type (.id / name). This is an internal service field, setting a value is not required. +- `___path___` (String) Resource path for CRUD operations. This is an internal service field, setting a value is not required. +- `cmd` (String) The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well. +- `comment` (String) +- `dns` (String) Set custom DNS servers +- `domain_name` (String) Container NIS domain name +- `entrypoint` (String) An ENTRYPOINT allows to specify executable to run when starting container. Example: /bin/sh +- `envlist` (String) list of environmental variables (configured under /container envs ) to be used with container +- `file` (String) container *tar.gz tarball if the container is imported from a file +- `hostname` (String) Container host name +- `logging` (Boolean) if set to yes, all container-generated output will be shown in the RouterOS log +- `mounts` (Set of String) Mounts from /container/mounts/ sub-menu to be used with this container +- `remote_image` (String) The container image name to be installed if an external registry is used (configured under /container/config set registry-url=...) +- `root_dir` (String) Used to save container store outside main memory +- `start_on_boot` (Boolean) Start the container on boot +- `stop_signal` (String) Signal to stop the container. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `user` (String) Sets the username used +- `workdir` (String) The working directory for cmd entrypoint + +### Read-Only + +- `arch` (String) The architecture of the container image +- `id` (String) The ID of this resource. +- `name` (String) Assign a name to the container +- `os` (String) The OS of the container image +- `status` (String) The status of the container +- `tag` (String) The tag of the container image + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) + + diff --git a/docs/resources/container_config.md b/docs/resources/container_config.md new file mode 100644 index 00000000..2e4de2cc --- /dev/null +++ b/docs/resources/container_config.md @@ -0,0 +1,24 @@ +# routeros_container_config (Resource) + + + + + +## Schema + +### Optional + +- `___id___` (Number) Resource ID type (.id / name). This is an internal service field, setting a value is not required. +- `___path___` (String) Resource path for CRUD operations. This is an internal service field, setting a value is not required. +- `layer_dir` (String) Container layers directory. +- `password` (String, Sensitive) Specifies the password for authentication (starting from ROS 7.8) +- `ram_high` (String) RAM usage limit. (0 for unlimited) +- `registry_url` (String) External registry url from where the container will be downloaded. +- `tmpdir` (String) Container extraction directory. +- `username` (String) Specifies the username for authentication (starting from ROS 7.8) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/container_envs.md b/docs/resources/container_envs.md new file mode 100644 index 00000000..91862e53 --- /dev/null +++ b/docs/resources/container_envs.md @@ -0,0 +1,24 @@ +# routeros_container_envs (Resource) + + + + + +## Schema + +### Required + +- `key` (String) Key of the environment variable. +- `name` (String) Name of the environment variables list. +- `value` (String) Value of the environment variable. + +### Optional + +- `___id___` (Number) Resource ID type (.id / name). This is an internal service field, setting a value is not required. +- `___path___` (String) Resource path for CRUD operations. This is an internal service field, setting a value is not required. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/container_mounts.md b/docs/resources/container_mounts.md new file mode 100644 index 00000000..d034d4ca --- /dev/null +++ b/docs/resources/container_mounts.md @@ -0,0 +1,24 @@ +# routeros_container_mounts (Resource) + + + + + +## Schema + +### Required + +- `dst` (String) Specifies destination path of the mount, which points to defined location in container +- `name` (String) Name of the mount. +- `src` (String) Specifies source path of the mount, which points to a RouterOS location + +### Optional + +- `___id___` (Number) Resource ID type (.id / name). This is an internal service field, setting a value is not required. +- `___path___` (String) Resource path for CRUD operations. This is an internal service field, setting a value is not required. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/examples/routeros_container/import.sh b/examples/routeros_container/import.sh new file mode 100644 index 00000000..ed149dc7 --- /dev/null +++ b/examples/routeros_container/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/container get [print show-ids]] +terraform import routeros_container.busybox "*1" \ No newline at end of file diff --git a/examples/routeros_container/resource.tf b/examples/routeros_container/resource.tf new file mode 100644 index 00000000..c07c6ced --- /dev/null +++ b/examples/routeros_container/resource.tf @@ -0,0 +1,8 @@ +resource "routeros_container" "busybox" { + remote_image = "library/busybox:1.35.0" + cmd = "/bin/httpd -f -p 8080" + interface = routeros_interface_veth.busybox.name + logging = true + root_dir = "/usb1-part1/containers/busybox/root" + start_on_boot = true +} diff --git a/examples/routeros_container_config/import.sh b/examples/routeros_container_config/import.sh new file mode 100644 index 00000000..9abaca8b --- /dev/null +++ b/examples/routeros_container_config/import.sh @@ -0,0 +1 @@ +terraform import routeros_container_config.config . \ No newline at end of file diff --git a/examples/routeros_container_config/resource.tf b/examples/routeros_container_config/resource.tf new file mode 100644 index 00000000..265279ef --- /dev/null +++ b/examples/routeros_container_config/resource.tf @@ -0,0 +1,6 @@ +resource "routeros_container_config" "config" { + registry_url = "https://registry-1.docker.io" + ram_high = "0" + tmpdir = "/usb1-part1/containers/tmp" + layer_dir = "/usb1-part1/containers/layers" +} diff --git a/examples/routeros_container_envs/import.sh b/examples/routeros_container_envs/import.sh new file mode 100644 index 00000000..4dc92837 --- /dev/null +++ b/examples/routeros_container_envs/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/container/envs get [print show-ids]] +terraform import routeros_container_envs.test_envs "*1" \ No newline at end of file diff --git a/examples/routeros_container_envs/resource.tf b/examples/routeros_container_envs/resource.tf new file mode 100644 index 00000000..08a63ef7 --- /dev/null +++ b/examples/routeros_container_envs/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_container_envs" "test_envs" { + name = "test_envs" + key = "TZ" + value = "UTC" +} diff --git a/examples/routeros_container_mounts/import.sh b/examples/routeros_container_mounts/import.sh new file mode 100644 index 00000000..41f7c607 --- /dev/null +++ b/examples/routeros_container_mounts/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/container/mounts get [print show-ids]] +terraform import routeros_container_mounts.caddyfile "*1" \ No newline at end of file diff --git a/examples/routeros_container_mounts/resource.tf b/examples/routeros_container_mounts/resource.tf new file mode 100644 index 00000000..f3ab8c41 --- /dev/null +++ b/examples/routeros_container_mounts/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_container_mounts" "caddyfile" { + name = "Caddyfile" + src = "/usb1-part1/containers/caddy/Caddyfile" + dst = "/etc/caddy/Caddyfile" +} diff --git a/routeros/mikrotik_client.go b/routeros/mikrotik_client.go index 858d58b5..e9b83071 100644 --- a/routeros/mikrotik_client.go +++ b/routeros/mikrotik_client.go @@ -34,6 +34,8 @@ const ( crudRemove crudRevoke crudMove + crudStart + crudStop ) func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { diff --git a/routeros/mikrotik_client_api.go b/routeros/mikrotik_client_api.go index 8934c70b..232bfccf 100644 --- a/routeros/mikrotik_client_api.go +++ b/routeros/mikrotik_client_api.go @@ -29,6 +29,8 @@ var ( crudRemove: "/remove", crudRevoke: "/issued-revoke", crudMove: "/move", + crudStart: "/start", + crudStop: "/stop", } ) diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index bfa677e5..7eb5cb8a 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -36,6 +36,8 @@ var ( crudRemove: "POST", crudRevoke: "POST", crudMove: "POST", + crudStart: "POST", + crudStop: "POST", } ) diff --git a/routeros/provider.go b/routeros/provider.go index 117a2cfd..670f39a1 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -185,6 +185,12 @@ func Provider() *schema.Provider { "routeros_capsman_rates": ResourceCapsManRates(), "routeros_capsman_security": ResourceCapsManSecurity(), + // Container objects + "routeros_container": ResourceContainer(), + "routeros_container_config": ResourceContainerConfig(), + "routeros_container_envs": ResourceContainerEnvs(), + "routeros_container_mounts": ResourceContainerMounts(), + // File objects "routeros_file": ResourceFile(), diff --git a/routeros/resource_container.go b/routeros/resource_container.go new file mode 100644 index 00000000..02fcca5f --- /dev/null +++ b/routeros/resource_container.go @@ -0,0 +1,281 @@ +package routeros + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// https://help.mikrotik.com/docs/display/ROS/Container#Container-Properties +func ResourceContainer() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/container"), + MetaId: PropId(Id), + + "arch": { + Type: schema.TypeString, + Computed: true, + Description: "The architecture of the container image", + }, + "cmd": { + Type: schema.TypeString, + Optional: true, + Description: "The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.", + }, + KeyComment: PropCommentRw, + "dns": { + Type: schema.TypeString, + Optional: true, + Description: "Set custom DNS servers", + }, + "domain_name": { + Type: schema.TypeString, + Optional: true, + Description: "Container NIS domain name", + }, + "entrypoint": { + Type: schema.TypeString, + Optional: true, + Description: "An ENTRYPOINT allows to specify executable to run when starting container. Example: /bin/sh", + }, + "envlist": { + Type: schema.TypeString, + Optional: true, + Description: "list of environmental variables (configured under /container envs ) to be used with container", + }, + "file": { + Type: schema.TypeString, + Optional: true, + Description: "container *tar.gz tarball if the container is imported from a file", + ExactlyOneOf: []string{"file", "remote_image"}, + }, + "hostname": { + Type: schema.TypeString, + Optional: true, + Description: "Container host name", + }, + "interface": { + Type: schema.TypeString, + Required: true, + Description: "veth interface to be used with the container", + }, + "logging": { + Type: schema.TypeBool, + Optional: true, + Description: "if set to yes, all container-generated output will be shown in the RouterOS log", + }, + "mounts": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Mounts from /container/mounts/ sub-menu to be used with this container", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Assign a name to the container", + }, + "os": { + Type: schema.TypeString, + Computed: true, + Description: "The OS of the container image", + }, + "remote_image": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The container image name to be installed if an external registry is used (configured under /container/config set registry-url=...)", + ExactlyOneOf: []string{"file", "remote_image"}, + }, + "root_dir": { + Type: schema.TypeString, + Optional: true, + Description: "Used to save container store outside main memory", + }, + "start_on_boot": { + Type: schema.TypeBool, + Optional: true, + Description: "Start the container on boot", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The status of the container", + }, + "stop_signal": { + Type: schema.TypeString, + Optional: true, + Description: "Signal to stop the container.", + }, + "tag": { + Type: schema.TypeString, + Computed: true, + Description: "The tag of the container image", + }, + "user": { + Type: schema.TypeString, + Optional: true, + Description: "Sets the username used", + }, + "workdir": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The working directory for cmd entrypoint", + }, + } + + resCreate := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Run DefaultCreate. + diags := ResourceCreate(ctx, resSchema, d, m) + if diags.HasError() { + return diags + } + + startContainer(ctx, resSchema, d, m) + + return ResourceRead(ctx, resSchema, d, m) + } + + resUpdate := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + stopContainer(ctx, resSchema, d, m) + + // Run DefaultUpdate. + diags := ResourceUpdate(ctx, resSchema, d, m) + if diags.HasError() { + return diags + } + startContainer(ctx, resSchema, d, m) + + return ResourceRead(ctx, resSchema, d, m) + } + + resDelete := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Stop container + stopContainer(ctx, resSchema, d, m) + + // Run DefaultDelete. + return ResourceDelete(ctx, resSchema, d, m) + } + + return &schema.Resource{ + CreateContext: resCreate, + ReadContext: DefaultRead(resSchema), + UpdateContext: resUpdate, + DeleteContext: resDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(1 * time.Minute), + }, + } +} + +func startContainer(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { + stopStateConf := &retry.StateChangeConf{ + Pending: []string{"pulling", "extracting"}, + Target: []string{"stopped"}, + Refresh: func() (result interface{}, state string, err error) { + metadata := GetMetadata(s) + + res, err := ReadItems(&ItemId{metadata.IdType, d.Id()}, metadata.Path, m.(Client)) + if err != nil { + return res, (*res)[0]["status"], err + } + + return res, (*res)[0]["status"], nil + }, + Timeout: d.Timeout(schema.TimeoutCreate), + } + _, err := stopStateConf.WaitForStateContext(ctx) + if err != nil { + err = fmt.Errorf("Error waiting for container instance (%s) to be pulled: %s", d.Id(), err) + return diag.FromErr(err) + } + + item := MikrotikItem{"number": d.Id()} + + // Start container + var resUrl = &URL{ + Path: s[MetaResourcePath].Default.(string), + } + if m.(Client).GetTransport() == TransportREST { + resUrl.Path += "/start" + } + + err = m.(Client).SendRequest(crudStart, resUrl, item, nil) + if err != nil { + return diag.FromErr(err) + } + + startStateConf := &retry.StateChangeConf{ + Pending: []string{"stopped"}, + Target: []string{"running"}, + Refresh: func() (result interface{}, state string, err error) { + metadata := GetMetadata(s) + + res, err := ReadItems(&ItemId{metadata.IdType, d.Id()}, metadata.Path, m.(Client)) + if err != nil { + return res, (*res)[0]["status"], err + } + + return res, (*res)[0]["status"], nil + }, + Timeout: d.Timeout(schema.TimeoutCreate), + } + _, err = startStateConf.WaitForStateContext(ctx) + if err != nil { + err = fmt.Errorf("Error waiting for container instance (%s) to be started: %s", d.Id(), err) + return diag.FromErr(err) + } + + return nil +} + +func stopContainer(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { + item := MikrotikItem{"number": d.Id()} + + var resUrl = &URL{ + Path: s[MetaResourcePath].Default.(string), + } + if m.(Client).GetTransport() == TransportREST { + resUrl.Path += "/stop" + } + + err := m.(Client).SendRequest(crudStop, resUrl, item, nil) + if err != nil { + return diag.FromErr(err) + } + + stopStateConf := &retry.StateChangeConf{ + Pending: []string{"stopping"}, + Target: []string{"stopped"}, + Refresh: func() (result interface{}, state string, err error) { + metadata := GetMetadata(s) + + res, err := ReadItems(&ItemId{metadata.IdType, d.Id()}, metadata.Path, m.(Client)) + if err != nil { + return res, (*res)[0]["status"], err + } + + return res, (*res)[0]["status"], nil + }, + Timeout: d.Timeout(schema.TimeoutDelete), + } + _, err = stopStateConf.WaitForStateContext(ctx) + if err != nil { + err = fmt.Errorf("Error waiting for container instance (%s) to be stopped: %s", d.Id(), err) + return diag.FromErr(err) + } + return nil +} diff --git a/routeros/resource_container_config.go b/routeros/resource_container_config.go new file mode 100644 index 00000000..96ad91b3 --- /dev/null +++ b/routeros/resource_container_config.go @@ -0,0 +1,59 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// https://help.mikrotik.com/docs/display/ROS/Container#Container-Containerconfiguration +func ResourceContainerConfig() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/container/config"), + MetaId: PropId(Name), + + "registry_url": { + Type: schema.TypeString, + Optional: true, + Description: "External registry url from where the container will be downloaded.", + }, + "username": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the username for authentication (starting from ROS 7.8)", + }, + "password": { + Type: schema.TypeString, + Sensitive: true, + Optional: true, + Description: "Specifies the password for authentication (starting from ROS 7.8)", + }, + "ram_high": { + Type: schema.TypeString, + Optional: true, + Default: "0", + Description: "RAM usage limit. (0 for unlimited)", + }, + "tmpdir": { + Type: schema.TypeString, + Optional: true, + Description: "Container extraction directory.", + }, + "layer_dir": { + Type: schema.TypeString, + Optional: true, + Description: "Container layers directory.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultSystemCreate(resSchema), + ReadContext: DefaultSystemRead(resSchema), + UpdateContext: DefaultSystemUpdate(resSchema), + DeleteContext: DefaultSystemDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_container_envs.go b/routeros/resource_container_envs.go new file mode 100644 index 00000000..47365eb0 --- /dev/null +++ b/routeros/resource_container_envs.go @@ -0,0 +1,71 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// https://help.mikrotik.com/docs/display/ROS/Container#Container-Addenvironmentvariablesandmounts(optional) +func ResourceContainerEnvs() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/container/envs"), + MetaId: PropId(Id), + + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the environment variables list.", + }, + "key": { + Type: schema.TypeString, + Required: true, + Description: "Key of the environment variable.", + }, + "value": { + Type: schema.TypeString, + Required: true, + Description: "Value of the environment variable.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} + +// TODO: cleaner would be if we model it like envlist directly +/* +resource "routeros_container_envs" "test" { + name = "test" + + env { + key = "foo" + value = "bar" + } + + env { + key = "hello" + value = "world" + } +} + +resource "routeros_container_envs" "test_foo" { + name = "test" + key = "foo" + value = "bar" + } + + resource "routeros_container_envs" "test_hello" { + name = "test" + key = "hello" + value = "wold" + } +*/ diff --git a/routeros/resource_container_mounts.go b/routeros/resource_container_mounts.go new file mode 100644 index 00000000..a3673267 --- /dev/null +++ b/routeros/resource_container_mounts.go @@ -0,0 +1,42 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// https://help.mikrotik.com/docs/display/ROS/Container#Container-Addenvironmentvariablesandmounts(optional) +func ResourceContainerMounts() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/container/mounts"), + MetaId: PropId(Name), + + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the mount.", + }, + "src": { + Type: schema.TypeString, + Required: true, + Description: "Specifies source path of the mount, which points to a RouterOS location", + }, + "dst": { + Type: schema.TypeString, + Required: true, + Description: "Specifies destination path of the mount, which points to defined location in container", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +}