pack/internal/build/container_ops_test.go

730 lines
22 KiB
Go

package build_test
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/buildpacks/lifecycle/platform/files"
"github.com/heroku/color"
dcontainer "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/buildpacks/pack/internal/build"
"github.com/buildpacks/pack/internal/builder"
"github.com/buildpacks/pack/internal/container"
h "github.com/buildpacks/pack/testhelpers"
)
var ctrClient *client.Client
// TestContainerOperations are integration tests for the container operations against a docker daemon
func TestContainerOperations(t *testing.T) {
color.Disable(true)
defer color.Disable(false)
h.RequireDocker(t)
var err error
ctrClient, err = client.New(client.FromEnv)
h.AssertNil(t, err)
spec.Run(t, "container-ops", testContainerOps, spec.Report(report.Terminal{}), spec.Sequential())
}
func testContainerOps(t *testing.T, when spec.G, it spec.S) {
var (
imageName string
osType string
)
it.Before(func() {
imageName = "container-ops.test-" + h.RandString(10)
infoResult, err := ctrClient.Info(context.TODO(), client.InfoOptions{})
h.AssertNil(t, err)
osType = infoResult.Info.OSType
dockerfileContent := `FROM busybox`
if osType == "windows" {
dockerfileContent = `FROM mcr.microsoft.com/windows/nanoserver:1809`
}
h.CreateImage(t, ctrClient, imageName, dockerfileContent)
h.AssertNil(t, err)
})
it.After(func() {
h.DockerRmi(ctrClient, imageName)
})
when("#CopyDir", func() {
it("writes contents with proper owner/permissions", func() {
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/some-vol"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /s c:\some-vol`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
// chmod in case umask sets the wrong bits during a `git clone`.
dir := filepath.Join("testdata", "fake-app")
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
return os.Chmod(path, 0644)
})
h.AssertNil(t, err)
copyDirOp := build.CopyDir(dir, containerDir, 123, 456, osType, false, nil)
var outBuf, errBuf bytes.Buffer
err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
// Expected WCOW results
h.AssertContainsMatch(t, strings.ReplaceAll(outBuf.String(), "\r", ""), `
(.*) <DIR> ... .
(.*) <DIR> ... ..
(.*) 17 ... fake-app-file
(.*) <SYMLINK> ... fake-app-symlink \[fake-app-file\]
(.*) 0 ... file-to-ignore
`)
} else {
if runtime.GOOS == "windows" {
// Expected LCOW results
h.AssertContainsMatch(t, outBuf.String(), `
-rwxrwxrwx 1 123 456 (.*) fake-app-file
lrwxrwxrwx 1 123 456 (.*) fake-app-symlink -> fake-app-file
-rwxrwxrwx 1 123 456 (.*) file-to-ignore
`)
} else {
// Expected results
h.AssertContainsMatch(t, outBuf.String(), `
-rw-r--r-- 1 123 456 (.*) fake-app-file
lrwxrwxrwx 1 123 456 (.*) fake-app-symlink -> fake-app-file
-rw-r--r-- 1 123 456 (.*) file-to-ignore
`)
}
}
})
when("includeRoot", func() {
it("copies root dir with new GID, UID and permissions", func() {
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q c:`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, true, nil)
var outBuf, errBuf bytes.Buffer
err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
// Expected WCOW results
h.AssertContainsMatch(t, strings.ReplaceAll(outBuf.String(), "\r", ""), `
(.*) <DIR> ... some-vol
`)
} else {
h.AssertContainsMatch(t, outBuf.String(), `
drwxrwxrwx 2 123 456 (.*) some-vol
`)
}
})
})
it("writes contents ignoring from file filter", func() {
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/some-vol"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /s /n c:\some-vol`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, false, func(filename string) bool {
return filepath.Base(filename) != "file-to-ignore"
})
var outBuf, errBuf bytes.Buffer
err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
h.AssertContains(t, outBuf.String(), "fake-app-file")
h.AssertNotContains(t, outBuf.String(), "file-to-ignore")
})
it("writes contents from zip file", func() {
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/some-vol"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /s /n c:\some-vol`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app.zip"), containerDir, 123, 456, osType, false, nil)
var outBuf, errBuf bytes.Buffer
err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
h.AssertContainsMatch(t, strings.ReplaceAll(outBuf.String(), "\r", ""), `
(.*) <DIR> ... .
(.*) <DIR> ... ..
(.*) 17 ... fake-app-file
`)
} else {
h.AssertContainsMatch(t, outBuf.String(), `
-rw-r--r-- 1 123 456 (.*) fake-app-file
`)
}
})
})
when("#CopyOut", func() {
it("reads the contents of a container directory", func() {
h.SkipIf(t, osType == "windows", "copying directories out of windows containers not yet supported")
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/some-vol"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /s c:\some-vol`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, false, nil)
err = copyDirOp(ctrClient, ctx, ctr.ID, io.Discard, io.Discard)
h.AssertNil(t, err)
tarDestination, err := os.CreateTemp("", "pack.container.ops.test.")
h.AssertNil(t, err)
defer os.RemoveAll(tarDestination.Name())
handler := func(reader io.ReadCloser) error {
defer reader.Close()
contents, err := io.ReadAll(reader)
h.AssertNil(t, err)
err = os.WriteFile(tarDestination.Name(), contents, 0600)
h.AssertNil(t, err)
return nil
}
copyOutDirsOp := build.CopyOut(handler, containerDir)
err = copyOutDirsOp(ctrClient, ctx, ctr.ID, io.Discard, io.Discard)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(io.Discard, io.Discard))
h.AssertNil(t, err)
separator := "/"
if osType == "windows" {
separator = `\`
}
h.AssertTarball(t, tarDestination.Name())
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfake-app-file", separator))
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfake-app-symlink", separator))
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfile-to-ignore", separator))
})
})
when("#CopyOutMaybe", func() {
it("reads the contents of a container directory", func() {
h.SkipIf(t, osType == "windows", "copying directories out of windows containers not yet supported")
containerDir := "/some-vol"
if osType == "windows" {
containerDir = `c:\some-vol`
}
ctrCmd := []string{"ls", "-al", "/some-vol"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /s c:\some-vol`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
copyDirOp := build.CopyDir(filepath.Join("testdata", "fake-app"), containerDir, 123, 456, osType, false, nil)
err = copyDirOp(ctrClient, ctx, ctr.ID, io.Discard, io.Discard)
h.AssertNil(t, err)
tarDestination, err := os.CreateTemp("", "pack.container.ops.test.")
h.AssertNil(t, err)
defer os.RemoveAll(tarDestination.Name())
handler := func(reader io.ReadCloser) error {
defer reader.Close()
contents, err := io.ReadAll(reader)
h.AssertNil(t, err)
err = os.WriteFile(tarDestination.Name(), contents, 0600)
h.AssertNil(t, err)
return nil
}
copyOutDirsOp := build.CopyOutMaybe(handler, containerDir)
err = copyOutDirsOp(ctrClient, ctx, ctr.ID, io.Discard, io.Discard)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(io.Discard, io.Discard))
h.AssertNil(t, err)
separator := "/"
if osType == "windows" {
separator = `\`
}
h.AssertTarball(t, tarDestination.Name())
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfake-app-file", separator))
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfake-app-symlink", separator))
h.AssertTarHasFile(t, tarDestination.Name(), fmt.Sprintf("some-vol%sfile-to-ignore", separator))
})
})
when("#WriteStackToml", func() {
it("writes file", func() {
containerDir := "/layers-vol"
containerPath := "/layers-vol/stack.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
containerPath = `c:\layers-vol\stack.toml`
}
ctrCmd := []string{"ls", "-al", "/layers-vol/stack.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /n c:\layers-vol\stack.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteStackToml(containerPath, builder.StackMetadata{
RunImage: builder.RunImageMetadata{
Image: "image-1",
Mirrors: []string{
"mirror-1",
"mirror-2",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
h.AssertContains(t, outBuf.String(), `01/01/1980 12:00 AM 69 ... stack.toml`)
} else {
h.AssertContains(t, outBuf.String(), `-rwxr-xr-x 1 root root 69 Jan 1 1980 /layers-vol/stack.toml`)
}
})
it("has expected contents", func() {
containerDir := "/layers-vol"
containerPath := "/layers-vol/stack.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
containerPath = `c:\layers-vol\stack.toml`
}
ctrCmd := []string{"cat", "/layers-vol/stack.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `type c:\layers-vol\stack.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteStackToml(containerPath, builder.StackMetadata{
RunImage: builder.RunImageMetadata{
Image: "image-1",
Mirrors: []string{
"mirror-1",
"mirror-2",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
h.AssertContains(t, outBuf.String(), `[run-image]
image = "image-1"
mirrors = ["mirror-1", "mirror-2"]
`)
})
})
when("#WriteRunToml", func() {
it("writes file", func() {
containerDir := "/layers-vol"
containerPath := "/layers-vol/run.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
containerPath = `c:\layers-vol\run.toml`
}
ctrCmd := []string{"ls", "-al", "/layers-vol/run.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /n c:\layers-vol\run.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{builder.RunImageMetadata{
Image: "image-1",
Mirrors: []string{
"mirror-1",
"mirror-2",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
h.AssertContains(t, outBuf.String(), `01/01/1980 12:00 AM 68 ... run.toml`)
} else {
h.AssertContains(t, outBuf.String(), `-rwxr-xr-x 1 root root 68 Jan 1 1980 /layers-vol/run.toml`)
}
})
it("has expected contents", func() {
containerDir := "/layers-vol"
containerPath := "/layers-vol/run.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
containerPath = `c:\layers-vol\run.toml`
}
ctrCmd := []string{"cat", "/layers-vol/run.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `type c:\layers-vol\run.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{
{
Image: "image-1",
Mirrors: []string{
"mirror-1",
"mirror-2",
},
},
{
Image: "image-2",
Mirrors: []string{
"mirror-3",
"mirror-4",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
h.AssertContains(t, outBuf.String(), `[[images]]
image = "image-1"
mirrors = ["mirror-1", "mirror-2"]
[[images]]
image = "image-2"
mirrors = ["mirror-3", "mirror-4"]
`)
})
})
when("#WriteProjectMetadata", func() {
it("writes file", func() {
containerDir := "/layers-vol"
p := "/layers-vol/project-metadata.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
p = `c:\layers-vol\project-metadata.toml`
}
ctrCmd := []string{"ls", "-al", "/layers-vol/project-metadata.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `dir /q /n c:\layers-vol\project-metadata.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteProjectMetadata(p, files.ProjectMetadata{
Source: &files.ProjectSource{
Type: "project",
Version: map[string]interface{}{
"declared": "1.0.2",
},
Metadata: map[string]interface{}{
"url": "https://github.com/buildpacks/pack",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
if osType == "windows" {
h.AssertContains(t, outBuf.String(), `01/01/1980 12:00 AM 137 ... project-metadata.toml`)
} else {
h.AssertContains(t, outBuf.String(), `-rwxr-xr-x 1 root root 137 Jan 1 1980 /layers-vol/project-metadata.toml`)
}
})
it("has expected contents", func() {
containerDir := "/layers-vol"
p := "/layers-vol/project-metadata.toml"
if osType == "windows" {
containerDir = `c:\layers-vol`
p = `c:\layers-vol\project-metadata.toml`
}
ctrCmd := []string{"cat", "/layers-vol/project-metadata.toml"}
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `type c:\layers-vol\project-metadata.toml`}
}
ctx := context.Background()
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
writeOp := build.WriteProjectMetadata(p, files.ProjectMetadata{
Source: &files.ProjectSource{
Type: "project",
Version: map[string]interface{}{
"declared": "1.0.2",
},
Metadata: map[string]interface{}{
"url": "https://github.com/buildpacks/pack",
},
},
}, osType)
var outBuf, errBuf bytes.Buffer
err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertEq(t, errBuf.String(), "")
h.AssertNil(t, err)
h.AssertContains(t, outBuf.String(), `[source]
type = "project"
[source.version]
declared = "1.0.2"
[source.metadata]
url = "https://github.com/buildpacks/pack"
`)
})
})
when("#EnsureVolumeAccess", func() {
it("changes owner of volume", func() {
h.SkipIf(t, osType != "windows", "no-op for linux")
ctx := context.Background()
ctrCmd := []string{"ls", "-al", "/my-volume"}
containerDir := "/my-volume"
if osType == "windows" {
ctrCmd = []string{"cmd", "/c", `icacls c:\my-volume`}
containerDir = `c:\my-volume`
}
ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...)
h.AssertNil(t, err)
defer cleanupContainer(ctx, ctr.ID)
inspectResult, err := ctrClient.ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
if err != nil {
return
}
inspect := inspectResult.Container
// use container's current volumes
var ctrVolumes []string
for _, m := range inspect.Mounts {
if m.Type == mount.TypeVolume {
ctrVolumes = append(ctrVolumes, m.Name)
}
}
var outBuf, errBuf bytes.Buffer
// reuse same volume twice to demonstrate multiple ops
initVolumeOp := build.EnsureVolumeAccess(123, 456, osType, ctrVolumes[0], ctrVolumes[0])
err = initVolumeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf)
h.AssertNil(t, err)
err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf))
h.AssertNil(t, err)
h.AssertEq(t, errBuf.String(), "")
h.AssertContains(t, outBuf.String(), `BUILTIN\Users:(OI)(CI)(F)`)
})
})
}
func createContainer(ctx context.Context, imageName, containerDir, osType string, cmd ...string) (client.ContainerCreateResult, error) {
isolationType := dcontainer.IsolationDefault
if osType == "windows" {
isolationType = dcontainer.IsolationProcess
}
return ctrClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &dcontainer.Config{
Image: imageName,
Cmd: cmd,
},
HostConfig: &dcontainer.HostConfig{
Binds: []string{fmt.Sprintf("%s:%s", fmt.Sprintf("tests-volume-%s", h.RandString(5)), filepath.ToSlash(containerDir))},
Isolation: isolationType,
},
})
}
func cleanupContainer(ctx context.Context, ctrID string) {
inspectResult, err := ctrClient.ContainerInspect(ctx, ctrID, client.ContainerInspectOptions{})
if err != nil {
return
}
inspect := inspectResult.Container
// remove container
_, err = ctrClient.ContainerRemove(ctx, ctrID, client.ContainerRemoveOptions{})
if err != nil {
return
}
// remove volumes
for _, m := range inspect.Mounts {
if m.Type == mount.TypeVolume {
_, err = ctrClient.VolumeRemove(ctx, m.Name, client.VolumeRemoveOptions{Force: true})
if err != nil {
return
}
}
}
}