Merge pull request #926 from TomSweeneyRedHat/pause

Add `kpod pause` and `kpod unpause`
This commit is contained in:
Daniel J Walsh 2017-09-27 09:33:22 -04:00 committed by GitHub
commit 214adee0ef
13 changed files with 444 additions and 11 deletions

View file

@ -52,6 +52,7 @@ It is currently in active development in the Kubernetes community through the [d
| [kpod-logs(1)](/docs/kpod-logs.1.md) | Display the logs of a container || | [kpod-logs(1)](/docs/kpod-logs.1.md) | Display the logs of a container ||
| [kpod-mount(1)](/docs/kpod-mount.1.md) | Mount a working container's root filesystem || | [kpod-mount(1)](/docs/kpod-mount.1.md) | Mount a working container's root filesystem ||
| [kpod-ps(1)](/docs/kpod-ps.1.md) | Prints out information about containers |[![...](/docs/play.png)](https://asciinema.org/a/bbT41kac6CwZ5giESmZLIaTLR)| | [kpod-ps(1)](/docs/kpod-ps.1.md) | Prints out information about containers |[![...](/docs/play.png)](https://asciinema.org/a/bbT41kac6CwZ5giESmZLIaTLR)|
| [kpod-pause(1)](/docs/kpod-pause.1.md) | Pause one or more running containers ||
| [kpod-pull(1)](/docs/kpod-pull.1.md) | Pull an image from a registry |[![...](/docs/play.png)](https://asciinema.org/a/lr4zfoynHJOUNu1KaXa1dwG2X)| | [kpod-pull(1)](/docs/kpod-pull.1.md) | Pull an image from a registry |[![...](/docs/play.png)](https://asciinema.org/a/lr4zfoynHJOUNu1KaXa1dwG2X)|
| [kpod-push(1)](/docs/kpod-push.1.md) | Push an image to a specified destination |[![...](/docs/play.png)](https://asciinema.org/a/133276)| | [kpod-push(1)](/docs/kpod-push.1.md) | Push an image to a specified destination |[![...](/docs/play.png)](https://asciinema.org/a/133276)|
| [kpod-rename(1)](/docs/kpod-rename.1.md) | Rename a container || | [kpod-rename(1)](/docs/kpod-rename.1.md) | Rename a container ||
@ -59,9 +60,10 @@ It is currently in active development in the Kubernetes community through the [d
| [kpod-rmi(1)](/docs/kpod-rmi.1.md) | Removes one or more images |[![...](/docs/play.png)](https://asciinema.org/a/133799)| | [kpod-rmi(1)](/docs/kpod-rmi.1.md) | Removes one or more images |[![...](/docs/play.png)](https://asciinema.org/a/133799)|
| [kpod-save(1)](/docs/kpod-save.1.md) | Saves an image to an archive |[![...](/docs/play.png)](https://asciinema.org/a/kp8kOaexEhEa20P1KLZ3L5X4g)| | [kpod-save(1)](/docs/kpod-save.1.md) | Saves an image to an archive |[![...](/docs/play.png)](https://asciinema.org/a/kp8kOaexEhEa20P1KLZ3L5X4g)|
| [kpod-stats(1)](/docs/kpod-stats.1.md) | Display a live stream of one or more containers' resource usage statistics|| | [kpod-stats(1)](/docs/kpod-stats.1.md) | Display a live stream of one or more containers' resource usage statistics||
| [kpod-stop(1)](/docs/kpod-stop.1.md) | Stops one or more running containers.|| | [kpod-stop(1)](/docs/kpod-stop.1.md) | Stops one or more running containers ||
| [kpod-tag(1)](/docs/kpod-tag.1.md) | Add an additional name to a local image |[![...](/docs/play.png)](https://asciinema.org/a/133803)| | [kpod-tag(1)](/docs/kpod-tag.1.md) | Add an additional name to a local image |[![...](/docs/play.png)](https://asciinema.org/a/133803)|
| [kpod-umount(1)](/docs/kpod-umount.1.md) | Unmount a working container's root filesystem || | [kpod-umount(1)](/docs/kpod-umount.1.md) | Unmount a working container's root filesystem ||
| [kpod-unpause(1)](/docs/kpod-unpause.1.md) | Unpause one or more running containers ||
| [kpod-version(1)](/docs/kpod-version.1.md) | Display the version information |[![...](/docs/play.png)](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)| | [kpod-version(1)](/docs/kpod-version.1.md) | Display the version information |[![...](/docs/play.png)](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)|
## Configuration ## Configuration

View file

@ -40,6 +40,7 @@ func main() {
loadCommand, loadCommand,
logsCommand, logsCommand,
mountCommand, mountCommand,
pauseCommand,
psCommand, psCommand,
pullCommand, pullCommand,
pushCommand, pushCommand,
@ -51,6 +52,7 @@ func main() {
stopCommand, stopCommand,
tagCommand, tagCommand,
umountCommand, umountCommand,
unpauseCommand,
versionCommand, versionCommand,
} }
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {

58
cmd/kpod/pause.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/kubernetes-incubator/cri-o/libkpod"
"github.com/pkg/errors"
"github.com/urfave/cli"
"os"
)
var (
pauseDescription = `
kpod pause
Pauses one or more running containers. The container name or ID can be used.
`
pauseCommand = cli.Command{
Name: "pause",
Usage: "Pauses all the processes in one or more containers",
Description: pauseDescription,
Action: pauseCmd,
ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
}
)
func pauseCmd(c *cli.Context) error {
args := c.Args()
if len(args) < 1 {
return errors.Errorf("you must provide at least one container name or id")
}
config, err := getConfig(c)
if err != nil {
return errors.Wrapf(err, "could not get config")
}
server, err := libkpod.New(config)
if err != nil {
return errors.Wrapf(err, "could not get container server")
}
defer server.Shutdown()
if err := server.Update(); err != nil {
return errors.Wrapf(err, "could not update list of containers")
}
var lastError error
for _, container := range c.Args() {
cid, err := server.ContainerPause(container)
if err != nil {
if lastError != nil {
fmt.Fprintln(os.Stderr, lastError)
}
lastError = errors.Wrapf(err, "failed to pause container %v", container)
} else {
fmt.Println(cid)
}
}
return lastError
}

58
cmd/kpod/unpause.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/kubernetes-incubator/cri-o/libkpod"
"github.com/pkg/errors"
"github.com/urfave/cli"
"os"
)
var (
unpauseDescription = `
kpod unpause
Unpauses one or more running containers. The container name or ID can be used.
`
unpauseCommand = cli.Command{
Name: "unpause",
Usage: "Unpause the processes in one or more containers",
Description: unpauseDescription,
Action: unpauseCmd,
ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
}
)
func unpauseCmd(c *cli.Context) error {
args := c.Args()
if len(args) < 1 {
return errors.Errorf("you must provide at least one container name or id")
}
config, err := getConfig(c)
if err != nil {
return errors.Wrapf(err, "could not get config")
}
server, err := libkpod.New(config)
if err != nil {
return errors.Wrapf(err, "could not get container server")
}
defer server.Shutdown()
if err := server.Update(); err != nil {
return errors.Wrapf(err, "could not update list of containers")
}
var lastError error
for _, container := range c.Args() {
cid, err := server.ContainerUnpause(container)
if err != nil {
if lastError != nil {
fmt.Fprintln(os.Stderr, lastError)
}
lastError = errors.Wrapf(err, "failed to unpause container %v", container)
} else {
fmt.Println(cid)
}
}
return lastError
}

View file

@ -349,6 +349,14 @@ _kpod_export() {
_complete_ "$options_with_args" "$boolean_options" _complete_ "$options_with_args" "$boolean_options"
} }
_kpod_pause() {
local options_with_args="
--help -h
"
local boolean_options=""
_complete_ "$options_with_args" "$boolean_options"
}
_kpod_ps() { _kpod_ps() {
local options_with_args=" local options_with_args="
--filter -f --filter -f
@ -374,6 +382,14 @@ _kpod_stop() {
_complete_ "$options_with_args" "$boolean_options" _complete_ "$options_with_args" "$boolean_options"
} }
_kpod_unpause() {
local options_with_args="
--help -h
"
local boolean_options=""
_complete_ "$options_with_args" "$boolean_options"
}
_complete_() { _complete_() {
local options_with_args=$1 local options_with_args=$1
local boolean_options="$2 -h --help" local boolean_options="$2 -h --help"
@ -424,6 +440,7 @@ _kpod_kpod() {
load load
logs logs
mount mount
pause
ps ps
pull pull
push push
@ -436,6 +453,7 @@ _kpod_kpod() {
tag tag
umount umount
unmount unmount
unpause
version version
" "

24
docs/kpod-pause.1.md Normal file
View file

@ -0,0 +1,24 @@
% kpod(1) kpod-pause - Pause one or more containers
% Dan Walsh
# kpod-pause "1" "September 2017" "kpod"
## NAME
kpod pause - Pause one or more containers
## SYNOPSIS
**kpod pause [OPTIONS] CONTAINER [...]**
## DESCRIPTION
Pauses all the processes in one or more containers. You may use container IDs or names as input.
## EXAMPLE
kpod pause mywebserver
kpod pause 860a4b23
## SEE ALSO
kpod(1), kpod-unpause(1)
## HISTORY
September 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com>

24
docs/kpod-unpause.1.md Normal file
View file

@ -0,0 +1,24 @@
% kpod(1) kpod-unpause - Unpause one or more containers
% Dan Walsh
# kpod-unpause "1" "September 2017" "kpod"
## NAME
kpod unpause - Unpause one or more containers
## SYNOPSIS
**kpod unpause [OPTIONS] CONTAINER [...]**
## DESCRIPTION
Unpauses the processes in one or more containers. You may use container IDs or names as input.
## EXAMPLE
kpod unpause mywebserver
kpod unpause 860a4b23
## SEE ALSO
kpod(1), kpod-pause(1)
## HISTORY
September 2017, Originally compiled by Dan Walsh <dwalsh@redhat.com>

46
libkpod/pause.go Normal file
View file

@ -0,0 +1,46 @@
package libkpod
import (
"github.com/kubernetes-incubator/cri-o/oci"
"github.com/pkg/errors"
)
// ContainerPause pauses a running container.
func (c *ContainerServer) ContainerPause(container string) (string, error) {
ctr, err := c.LookupContainer(container)
if err != nil {
return "", errors.Wrapf(err, "failed to find container %s", container)
}
cStatus := c.runtime.ContainerStatus(ctr)
if cStatus.Status != oci.ContainerStatePaused {
if err := c.runtime.PauseContainer(ctr); err != nil {
return "", errors.Wrapf(err, "failed to pause container %s", ctr.ID())
}
c.ContainerStateToDisk(ctr)
} else {
return "", errors.Wrapf(err, "container %s is already paused", ctr.ID())
}
return ctr.ID(), nil
}
// ContainerUnpause unpauses a running container with a grace period (i.e., timeout).
func (c *ContainerServer) ContainerUnpause(container string) (string, error) {
ctr, err := c.LookupContainer(container)
if err != nil {
return "", errors.Wrapf(err, "failed to find container %s", container)
}
cStatus := c.runtime.ContainerStatus(ctr)
if cStatus.Status == oci.ContainerStatePaused {
if err := c.runtime.UnpauseContainer(ctr); err != nil {
return "", errors.Wrapf(err, "failed to unpause container %s", ctr.ID())
}
c.ContainerStateToDisk(ctr)
} else {
return "", errors.Wrapf(err, "the container %s is not paused", ctr.ID())
}
return ctr.ID(), nil
}

View file

@ -16,8 +16,11 @@ func (c *ContainerServer) Remove(container string, force bool) (string, error) {
} }
ctrID := ctr.ID() ctrID := ctr.ID()
cState := c.runtime.ContainerStatus(ctr) cStatus := c.runtime.ContainerStatus(ctr)
if cState.Status == oci.ContainerStateCreated || cState.Status == oci.ContainerStateRunning { switch cStatus.Status {
case oci.ContainerStatePaused:
return "", errors.Errorf("cannot remove paused container %s", ctrID)
case oci.ContainerStateCreated, oci.ContainerStateRunning:
if force { if force {
_, err = c.ContainerStop(container, -1) _, err = c.ContainerStop(container, -1)
if err != nil { if err != nil {

View file

@ -11,18 +11,25 @@ func (c *ContainerServer) ContainerStop(container string, timeout int64) (string
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to find container %s", container) return "", errors.Wrapf(err, "failed to find container %s", container)
} }
ctrID := ctr.ID()
cStatus := c.runtime.ContainerStatus(ctr) cStatus := c.runtime.ContainerStatus(ctr)
switch cStatus.Status {
case oci.ContainerStatePaused:
return "", errors.Errorf("cannot stop paused container %s", ctrID)
default:
if cStatus.Status != oci.ContainerStateStopped { if cStatus.Status != oci.ContainerStateStopped {
if err := c.runtime.StopContainer(ctr, timeout); err != nil { if err := c.runtime.StopContainer(ctr, timeout); err != nil {
return "", errors.Wrapf(err, "failed to stop container %s", ctr.ID()) return "", errors.Wrapf(err, "failed to stop container %s", ctrID)
}
if err := c.storageRuntimeServer.StopContainer(ctrID); err != nil {
return "", errors.Wrapf(err, "failed to unmount container %s", ctrID)
} }
if err := c.storageRuntimeServer.StopContainer(ctr.ID()); err != nil {
return "", errors.Wrapf(err, "failed to unmount container %s", ctr.ID())
} }
} }
c.ContainerStateToDisk(ctr) c.ContainerStateToDisk(ctr)
return ctr.ID(), nil return ctrID, nil
} }

View file

@ -24,6 +24,8 @@ import (
const ( const (
// ContainerStateCreated represents the created state of a container // ContainerStateCreated represents the created state of a container
ContainerStateCreated = "created" ContainerStateCreated = "created"
// ContainerStatePaused represents the paused state of a container
ContainerStatePaused = "paused"
// ContainerStateRunning represents the running state of a container // ContainerStateRunning represents the running state of a container
ContainerStateRunning = "running" ContainerStateRunning = "running"
// ContainerStateStopped represents the stopped state of a container // ContainerStateStopped represents the stopped state of a container
@ -696,3 +698,19 @@ func (r *Runtime) RuntimeReady() (bool, error) {
func (r *Runtime) NetworkReady() (bool, error) { func (r *Runtime) NetworkReady() (bool, error) {
return true, nil return true, nil
} }
// PauseContainer pauses a container.
func (r *Runtime) PauseContainer(c *Container) error {
c.opLock.Lock()
defer c.opLock.Unlock()
_, err := utils.ExecCmd(r.Path(c), "pause", c.id)
return err
}
// UnpauseContainer unpauses a container.
func (r *Runtime) UnpauseContainer(c *Container) error {
c.opLock.Lock()
defer c.opLock.Unlock()
_, err := utils.ExecCmd(r.Path(c), "resume", c.id)
return err
}

168
test/kpod_pause.bats Normal file
View file

@ -0,0 +1,168 @@
#!/usr/bin/env bats
load helpers
IMAGE="redis:alpine"
ROOT="$TESTDIR/crio"
RUNROOT="$TESTDIR/crio-run"
KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT $STORAGE_OPTS --runtime $RUNTIME_BINARY"
function teardown() {
cleanup_test
}
@test "pause a bogus container" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause foobar
echo "$output"
[ "$status" -eq 1 ]
}
@test "unpause a bogus container" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause foobar
echo "$output"
[ "$status" -eq 1 ]
}
@test "pause a created container by id" {
start_crio
run crioctl pod run --config "$TESTDATA"/sandbox_config.json
echo "$output"
[ "$status" -eq 0 ]
pod_id="$output"
run crioctl image pull "$IMAGE"
[ "$status" -eq 0 ]
run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id"
echo "$output"
[ "$status" -eq 0 ]
ctr_id="$output"
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} ps -a --filter id="$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
cleanup_pods
stop_crio
}
@test "pause a running container by id" {
start_crio
run crioctl pod run --config "$TESTDATA"/sandbox_config.json
echo "$output"
[ "$status" -eq 0 ]
pod_id="$output"
run crioctl image pull "$IMAGE"
[ "$status" -eq 0 ]
run crioctl ctr create --config "$TESTDATA"/container_redis.json --pod "$pod_id"
echo "$output"
[ "$status" -eq 0 ]
ctr_id="$output"
run crioctl ctr start --id "$ctr_id"
echo "$output"
id="$output"
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause "$id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause "$id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} ps -a --filter id="$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
cleanup_pods
stop_crio
}
@test "pause a running container by name" {
start_crio
run crioctl pod run --config "$TESTDATA"/sandbox_config.json
echo "$output"
[ "$status" -eq 0 ]
pod_id="$output"
run crioctl image pull "$IMAGE"
[ "$status" -eq 0 ]
run crioctl ctr create --config "$TESTDATA"/container_redis.json --pod "$pod_id"
echo "$output"
[ "$status" -eq 0 ]
ctr_id="$output"
run crioctl ctr start --id "$ctr_id"
echo "$output"
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause "k8s_podsandbox1-redis_podsandbox1_redhat.test.crio_redhat-test-crio_0"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause "k8s_podsandbox1-redis_podsandbox1_redhat.test.crio_redhat-test-crio_0"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} ps -a --filter id="k8s_podsandbox1-redis_podsandbox1_redhat.test.crio_redhat-test-crio_0"
echo "$output"
[ "$status" -eq 0 ]
cleanup_pods
stop_crio
}
@test "remove a paused container by id" {
start_crio
run crioctl pod run --config "$TESTDATA"/sandbox_config.json
echo "$output"
[ "$status" -eq 0 ]
pod_id="$output"
run crioctl image pull "$IMAGE"
[ "$status" -eq 0 ]
run crioctl ctr create --config "$TESTDATA"/container_redis.json --pod "$pod_id"
echo "$output"
[ "$status" -eq 0 ]
ctr_id="$output"
run crioctl ctr start --id "$ctr_id"
echo "$output"
id="$output"
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause "$id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} rm "$id"
echo "$output"
[ "$status" -eq 1 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} rm --force "$id"
echo "$output"
[ "$status" -eq 1 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause "$id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} stop "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} rm "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
cleanup_pods
stop_crio
}
@test "stop a paused container created by id" {
start_crio
run crioctl pod run --config "$TESTDATA"/sandbox_config.json
echo "$output"
[ "$status" -eq 0 ]
pod_id="$output"
run crioctl image pull "$IMAGE"
[ "$status" -eq 0 ]
run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id"
echo "$output"
[ "$status" -eq 0 ]
ctr_id="$output"
run ${KPOD_BINARY} ${KPOD_OPTIONS} pause "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} stop "$ctr_id"
echo "$output"
[ "$status" -eq 1 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} unpause "$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} ps -a --filter id="$ctr_id"
echo "$output"
[ "$status" -eq 0 ]
cleanup_pods
stop_crio
}

View file

@ -40,12 +40,13 @@ There are other equivalents for these tools
| Existing Step | CRI-O (and friends) | | Existing Step | CRI-O (and friends) |
| :---: | :---: | | :---: | :---: |
| `docker build` | [`buildah bud`](https://github.com/projectatomic/buildah/blob/master/docs/buildah-bud.md) | | `docker build` | [`buildah bud`](https://github.com/projectatomic/buildah/blob/master/docs/buildah-bud.md) |
| `docker cp` | [`kpod mount`](./docs/kpod-cp.1.md) | | `docker cp` | [`kpod mount`](./docs/kpod-cp.1.md) *** |
| `docker diff` | [`kpod diff`](./docs/kpod-diff.1.md) | | `docker diff` | [`kpod diff`](./docs/kpod-diff.1.md) |
| `docker export` | [`kpod export`](./docs/kpod-export.1.md) | | `docker export` | [`kpod export`](./docs/kpod-export.1.md) |
| `docker history`| [`kpod history`](./docs/kpod-history.1.md)| | `docker history`| [`kpod history`](./docs/kpod-history.1.md)|
| `docker images` | [`kpod images`](./docs/kpod-images.1.md) | | `docker images` | [`kpod images`](./docs/kpod-images.1.md) |
| `docker load` | [`kpod load`](./docs/kpod-load.1.md) | | `docker load` | [`kpod load`](./docs/kpod-load.1.md) |
| `docker pause` | [`kpod pause`](./docs/kpod-pause.1.md) |
| `docker ps` | [`kpod ps`](./docs/kpod-ps.1.md) | | `docker ps` | [`kpod ps`](./docs/kpod-ps.1.md) |
| `docker pull` | [`kpod pull`](./docs/kpod-pull.1.md) | | `docker pull` | [`kpod pull`](./docs/kpod-pull.1.md) |
| `docker push` | [`kpod push`](./docs/kpod-push.1.md) | | `docker push` | [`kpod push`](./docs/kpod-push.1.md) |
@ -54,4 +55,8 @@ There are other equivalents for these tools
| `docker rmi` | [`kpod rmi`](./docs/kpod-rmi.1.md) | | `docker rmi` | [`kpod rmi`](./docs/kpod-rmi.1.md) |
| `docker save` | [`kpod save`](./docs/kpod-save.1.md) | | `docker save` | [`kpod save`](./docs/kpod-save.1.md) |
| `docker tag` | [`kpod tag`](./docs/kpod-tag.1.md) | | `docker tag` | [`kpod tag`](./docs/kpod-tag.1.md) |
| `docker stop` | [`kpod stop`](./docs/kpod-stop.1.md) |
| `docker unpause`| [`kpod unpause`](./docs/kpod-unpause.1.md)|
| `docker version`| [`kpod version`](./docs/kpod-version.1.md)| | `docker version`| [`kpod version`](./docs/kpod-version.1.md)|
*** Use mount to take advantage of the entire linux tool chain rather then just cp. Read [`here`](./docs/kpod-cp.1.md) for more information.