The TestPodAnnotationPassthroughContainerSpec test and the TestContainerAnnotationPassthroughContainerSpec test both depend on a platform-specific implementation of criService.containerSpec, which is unimplemented on FreeBSD. The TestSandboxContainerSpec depends on a platform-specific implementation oc criService.sandboxContainerSpec, which is unimplemented on FreeBSD. Signed-off-by: Samuel Karp <me@samuelkarp.com>
		
			
				
	
	
		
			459 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package server
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"path/filepath"
 | 
						|
	goruntime "runtime"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	imagespec "github.com/opencontainers/image-spec/specs-go/v1"
 | 
						|
	runtimespec "github.com/opencontainers/runtime-spec/specs-go"
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
 | 
						|
 | 
						|
	"github.com/containerd/containerd/oci"
 | 
						|
	"github.com/containerd/containerd/pkg/cri/config"
 | 
						|
	"github.com/containerd/containerd/pkg/cri/constants"
 | 
						|
	"github.com/containerd/containerd/pkg/cri/opts"
 | 
						|
)
 | 
						|
 | 
						|
func checkMount(t *testing.T, mounts []runtimespec.Mount, src, dest, typ string,
 | 
						|
	contains, notcontains []string) {
 | 
						|
	found := false
 | 
						|
	for _, m := range mounts {
 | 
						|
		if m.Source == src && m.Destination == dest {
 | 
						|
			assert.Equal(t, m.Type, typ)
 | 
						|
			for _, c := range contains {
 | 
						|
				assert.Contains(t, m.Options, c)
 | 
						|
			}
 | 
						|
			for _, n := range notcontains {
 | 
						|
				assert.NotContains(t, m.Options, n)
 | 
						|
			}
 | 
						|
			found = true
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	assert.True(t, found, "mount from %q to %q not found", src, dest)
 | 
						|
}
 | 
						|
 | 
						|
const testImageName = "container-image-name"
 | 
						|
 | 
						|
func TestGeneralContainerSpec(t *testing.T) {
 | 
						|
	testID := "test-id"
 | 
						|
	testPid := uint32(1234)
 | 
						|
	containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
 | 
						|
	ociRuntime := config.Runtime{}
 | 
						|
	c := newTestCRIService()
 | 
						|
	testSandboxID := "sandbox-id"
 | 
						|
	testContainerName := "container-name"
 | 
						|
	spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime)
 | 
						|
	require.NoError(t, err)
 | 
						|
	specCheck(t, testID, testSandboxID, testPid, spec)
 | 
						|
}
 | 
						|
 | 
						|
func TestPodAnnotationPassthroughContainerSpec(t *testing.T) {
 | 
						|
	switch goruntime.GOOS {
 | 
						|
	case "darwin":
 | 
						|
		t.Skip("not implemented on Darwin")
 | 
						|
	case "freebsd":
 | 
						|
		t.Skip("not implemented on FreeBSD")
 | 
						|
	}
 | 
						|
 | 
						|
	testID := "test-id"
 | 
						|
	testSandboxID := "sandbox-id"
 | 
						|
	testContainerName := "container-name"
 | 
						|
	testPid := uint32(1234)
 | 
						|
 | 
						|
	for desc, test := range map[string]struct {
 | 
						|
		podAnnotations []string
 | 
						|
		configChange   func(*runtime.PodSandboxConfig)
 | 
						|
		specCheck      func(*testing.T, *runtimespec.Spec)
 | 
						|
	}{
 | 
						|
		"a passthrough annotation should be passed as an OCI annotation": {
 | 
						|
			podAnnotations: []string{"c"},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				assert.Equal(t, spec.Annotations["c"], "d")
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"a non-passthrough annotation should not be passed as an OCI annotation": {
 | 
						|
			configChange: func(c *runtime.PodSandboxConfig) {
 | 
						|
				c.Annotations["d"] = "e"
 | 
						|
			},
 | 
						|
			podAnnotations: []string{"c"},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				assert.Equal(t, spec.Annotations["c"], "d")
 | 
						|
				_, ok := spec.Annotations["d"]
 | 
						|
				assert.False(t, ok)
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"passthrough annotations should support wildcard match": {
 | 
						|
			configChange: func(c *runtime.PodSandboxConfig) {
 | 
						|
				c.Annotations["t.f"] = "j"
 | 
						|
				c.Annotations["z.g"] = "o"
 | 
						|
				c.Annotations["z"] = "o"
 | 
						|
				c.Annotations["y.ca"] = "b"
 | 
						|
				c.Annotations["y"] = "b"
 | 
						|
			},
 | 
						|
			podAnnotations: []string{"t*", "z.*", "y.c*"},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				t.Logf("%+v", spec.Annotations)
 | 
						|
				assert.Equal(t, spec.Annotations["t.f"], "j")
 | 
						|
				assert.Equal(t, spec.Annotations["z.g"], "o")
 | 
						|
				assert.Equal(t, spec.Annotations["y.ca"], "b")
 | 
						|
				_, ok := spec.Annotations["y"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["z"]
 | 
						|
				assert.False(t, ok)
 | 
						|
			},
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(desc, func(t *testing.T) {
 | 
						|
			c := newTestCRIService()
 | 
						|
			containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
 | 
						|
			if test.configChange != nil {
 | 
						|
				test.configChange(sandboxConfig)
 | 
						|
			}
 | 
						|
 | 
						|
			ociRuntime := config.Runtime{
 | 
						|
				PodAnnotations: test.podAnnotations,
 | 
						|
			}
 | 
						|
			spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName,
 | 
						|
				containerConfig, sandboxConfig, imageConfig, nil, ociRuntime)
 | 
						|
			assert.NoError(t, err)
 | 
						|
			assert.NotNil(t, spec)
 | 
						|
			specCheck(t, testID, testSandboxID, testPid, spec)
 | 
						|
			if test.specCheck != nil {
 | 
						|
				test.specCheck(t, spec)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestContainerSpecCommand(t *testing.T) {
 | 
						|
	for desc, test := range map[string]struct {
 | 
						|
		criEntrypoint   []string
 | 
						|
		criArgs         []string
 | 
						|
		imageEntrypoint []string
 | 
						|
		imageArgs       []string
 | 
						|
		expected        []string
 | 
						|
		expectErr       bool
 | 
						|
	}{
 | 
						|
		"should use cri entrypoint if it's specified": {
 | 
						|
			criEntrypoint:   []string{"a", "b"},
 | 
						|
			imageEntrypoint: []string{"c", "d"},
 | 
						|
			imageArgs:       []string{"e", "f"},
 | 
						|
			expected:        []string{"a", "b"},
 | 
						|
		},
 | 
						|
		"should use cri entrypoint if it's specified even if it's empty": {
 | 
						|
			criEntrypoint:   []string{},
 | 
						|
			criArgs:         []string{"a", "b"},
 | 
						|
			imageEntrypoint: []string{"c", "d"},
 | 
						|
			imageArgs:       []string{"e", "f"},
 | 
						|
			expected:        []string{"a", "b"},
 | 
						|
		},
 | 
						|
		"should use cri entrypoint and args if they are specified": {
 | 
						|
			criEntrypoint:   []string{"a", "b"},
 | 
						|
			criArgs:         []string{"c", "d"},
 | 
						|
			imageEntrypoint: []string{"e", "f"},
 | 
						|
			imageArgs:       []string{"g", "h"},
 | 
						|
			expected:        []string{"a", "b", "c", "d"},
 | 
						|
		},
 | 
						|
		"should use image entrypoint if cri entrypoint is not specified": {
 | 
						|
			criArgs:         []string{"a", "b"},
 | 
						|
			imageEntrypoint: []string{"c", "d"},
 | 
						|
			imageArgs:       []string{"e", "f"},
 | 
						|
			expected:        []string{"c", "d", "a", "b"},
 | 
						|
		},
 | 
						|
		"should use image args if both cri entrypoint and args are not specified": {
 | 
						|
			imageEntrypoint: []string{"c", "d"},
 | 
						|
			imageArgs:       []string{"e", "f"},
 | 
						|
			expected:        []string{"c", "d", "e", "f"},
 | 
						|
		},
 | 
						|
		"should return error if both entrypoint and args are empty": {
 | 
						|
			expectErr: true,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(desc, func(t *testing.T) {
 | 
						|
			config, _, imageConfig, _ := getCreateContainerTestData()
 | 
						|
			config.Command = test.criEntrypoint
 | 
						|
			config.Args = test.criArgs
 | 
						|
			imageConfig.Entrypoint = test.imageEntrypoint
 | 
						|
			imageConfig.Cmd = test.imageArgs
 | 
						|
 | 
						|
			var spec runtimespec.Spec
 | 
						|
			err := opts.WithProcessArgs(config, imageConfig)(context.Background(), nil, nil, &spec)
 | 
						|
			if test.expectErr {
 | 
						|
				assert.Error(t, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			assert.NoError(t, err)
 | 
						|
			assert.Equal(t, test.expected, spec.Process.Args, desc)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestVolumeMounts(t *testing.T) {
 | 
						|
	testContainerRootDir := "test-container-root"
 | 
						|
	for desc, test := range map[string]struct {
 | 
						|
		criMounts         []*runtime.Mount
 | 
						|
		imageVolumes      map[string]struct{}
 | 
						|
		expectedMountDest []string
 | 
						|
	}{
 | 
						|
		"should setup rw mount for image volumes": {
 | 
						|
			imageVolumes: map[string]struct{}{
 | 
						|
				"/test-volume-1": {},
 | 
						|
				"/test-volume-2": {},
 | 
						|
			},
 | 
						|
			expectedMountDest: []string{
 | 
						|
				"/test-volume-1",
 | 
						|
				"/test-volume-2",
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"should skip image volumes if already mounted by CRI": {
 | 
						|
			criMounts: []*runtime.Mount{
 | 
						|
				{
 | 
						|
					ContainerPath: "/test-volume-1",
 | 
						|
					HostPath:      "/test-hostpath-1",
 | 
						|
				},
 | 
						|
			},
 | 
						|
			imageVolumes: map[string]struct{}{
 | 
						|
				"/test-volume-1": {},
 | 
						|
				"/test-volume-2": {},
 | 
						|
			},
 | 
						|
			expectedMountDest: []string{
 | 
						|
				"/test-volume-2",
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"should compare and return cleanpath": {
 | 
						|
			criMounts: []*runtime.Mount{
 | 
						|
				{
 | 
						|
					ContainerPath: "/test-volume-1",
 | 
						|
					HostPath:      "/test-hostpath-1",
 | 
						|
				},
 | 
						|
			},
 | 
						|
			imageVolumes: map[string]struct{}{
 | 
						|
				"/test-volume-1/": {},
 | 
						|
				"/test-volume-2/": {},
 | 
						|
			},
 | 
						|
			expectedMountDest: []string{
 | 
						|
				"/test-volume-2/",
 | 
						|
			},
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(desc, func(t *testing.T) {
 | 
						|
			config := &imagespec.ImageConfig{
 | 
						|
				Volumes: test.imageVolumes,
 | 
						|
			}
 | 
						|
			c := newTestCRIService()
 | 
						|
			got := c.volumeMounts(testContainerRootDir, test.criMounts, config)
 | 
						|
			assert.Len(t, got, len(test.expectedMountDest))
 | 
						|
			for _, dest := range test.expectedMountDest {
 | 
						|
				found := false
 | 
						|
				for _, m := range got {
 | 
						|
					if m.ContainerPath == dest {
 | 
						|
						found = true
 | 
						|
						assert.Equal(t,
 | 
						|
							filepath.Dir(m.HostPath),
 | 
						|
							filepath.Join(testContainerRootDir, "volumes"))
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
				assert.True(t, found)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestContainerAnnotationPassthroughContainerSpec(t *testing.T) {
 | 
						|
	switch goruntime.GOOS {
 | 
						|
	case "darwin":
 | 
						|
		t.Skip("not implemented on Darwin")
 | 
						|
	case "freebsd":
 | 
						|
		t.Skip("not implemented on FreeBSD")
 | 
						|
	}
 | 
						|
 | 
						|
	testID := "test-id"
 | 
						|
	testSandboxID := "sandbox-id"
 | 
						|
	testContainerName := "container-name"
 | 
						|
	testPid := uint32(1234)
 | 
						|
 | 
						|
	for desc, test := range map[string]struct {
 | 
						|
		podAnnotations       []string
 | 
						|
		containerAnnotations []string
 | 
						|
		podConfigChange      func(*runtime.PodSandboxConfig)
 | 
						|
		configChange         func(*runtime.ContainerConfig)
 | 
						|
		specCheck            func(*testing.T, *runtimespec.Spec)
 | 
						|
	}{
 | 
						|
		"passthrough annotations from pod and container should be passed as an OCI annotation": {
 | 
						|
			podConfigChange: func(p *runtime.PodSandboxConfig) {
 | 
						|
				p.Annotations["pod.annotation.1"] = "1"
 | 
						|
				p.Annotations["pod.annotation.2"] = "2"
 | 
						|
				p.Annotations["pod.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			configChange: func(c *runtime.ContainerConfig) {
 | 
						|
				c.Annotations["container.annotation.1"] = "1"
 | 
						|
				c.Annotations["container.annotation.2"] = "2"
 | 
						|
				c.Annotations["container.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			podAnnotations:       []string{"pod.annotation.1"},
 | 
						|
			containerAnnotations: []string{"container.annotation.1"},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				assert.Equal(t, "1", spec.Annotations["container.annotation.1"])
 | 
						|
				_, ok := spec.Annotations["container.annotation.2"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["container.annotation.3"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				assert.Equal(t, "1", spec.Annotations["pod.annotation.1"])
 | 
						|
				_, ok = spec.Annotations["pod.annotation.2"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["pod.annotation.3"]
 | 
						|
				assert.False(t, ok)
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"passthrough annotations from pod and container should support wildcard": {
 | 
						|
			podConfigChange: func(p *runtime.PodSandboxConfig) {
 | 
						|
				p.Annotations["pod.annotation.1"] = "1"
 | 
						|
				p.Annotations["pod.annotation.2"] = "2"
 | 
						|
				p.Annotations["pod.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			configChange: func(c *runtime.ContainerConfig) {
 | 
						|
				c.Annotations["container.annotation.1"] = "1"
 | 
						|
				c.Annotations["container.annotation.2"] = "2"
 | 
						|
				c.Annotations["container.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			podAnnotations:       []string{"pod.annotation.*"},
 | 
						|
			containerAnnotations: []string{"container.annotation.*"},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				assert.Equal(t, "1", spec.Annotations["container.annotation.1"])
 | 
						|
				assert.Equal(t, "2", spec.Annotations["container.annotation.2"])
 | 
						|
				assert.Equal(t, "3", spec.Annotations["container.annotation.3"])
 | 
						|
				assert.Equal(t, "1", spec.Annotations["pod.annotation.1"])
 | 
						|
				assert.Equal(t, "2", spec.Annotations["pod.annotation.2"])
 | 
						|
				assert.Equal(t, "3", spec.Annotations["pod.annotation.3"])
 | 
						|
			},
 | 
						|
		},
 | 
						|
		"annotations should not pass through if no passthrough annotations are configured": {
 | 
						|
			podConfigChange: func(p *runtime.PodSandboxConfig) {
 | 
						|
				p.Annotations["pod.annotation.1"] = "1"
 | 
						|
				p.Annotations["pod.annotation.2"] = "2"
 | 
						|
				p.Annotations["pod.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			configChange: func(c *runtime.ContainerConfig) {
 | 
						|
				c.Annotations["container.annotation.1"] = "1"
 | 
						|
				c.Annotations["container.annotation.2"] = "2"
 | 
						|
				c.Annotations["container.annotation.3"] = "3"
 | 
						|
			},
 | 
						|
			podAnnotations:       []string{},
 | 
						|
			containerAnnotations: []string{},
 | 
						|
			specCheck: func(t *testing.T, spec *runtimespec.Spec) {
 | 
						|
				_, ok := spec.Annotations["container.annotation.1"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["container.annotation.2"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["container.annotation.3"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["pod.annotation.1"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["pod.annotation.2"]
 | 
						|
				assert.False(t, ok)
 | 
						|
				_, ok = spec.Annotations["pod.annotation.3"]
 | 
						|
				assert.False(t, ok)
 | 
						|
			},
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(desc, func(t *testing.T) {
 | 
						|
			c := newTestCRIService()
 | 
						|
			containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
 | 
						|
			if test.configChange != nil {
 | 
						|
				test.configChange(containerConfig)
 | 
						|
			}
 | 
						|
			if test.podConfigChange != nil {
 | 
						|
				test.podConfigChange(sandboxConfig)
 | 
						|
			}
 | 
						|
			ociRuntime := config.Runtime{
 | 
						|
				PodAnnotations:       test.podAnnotations,
 | 
						|
				ContainerAnnotations: test.containerAnnotations,
 | 
						|
			}
 | 
						|
			spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName,
 | 
						|
				containerConfig, sandboxConfig, imageConfig, nil, ociRuntime)
 | 
						|
			assert.NoError(t, err)
 | 
						|
			assert.NotNil(t, spec)
 | 
						|
			specCheck(t, testID, testSandboxID, testPid, spec)
 | 
						|
			if test.specCheck != nil {
 | 
						|
				test.specCheck(t, spec)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestBaseRuntimeSpec(t *testing.T) {
 | 
						|
	c := newTestCRIService()
 | 
						|
	c.baseOCISpecs = map[string]*oci.Spec{
 | 
						|
		"/etc/containerd/cri-base.json": {
 | 
						|
			Version:  "1.0.2",
 | 
						|
			Hostname: "old",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	out, err := c.runtimeSpec("id1", "/etc/containerd/cri-base.json", oci.WithHostname("new"))
 | 
						|
	assert.NoError(t, err)
 | 
						|
 | 
						|
	assert.Equal(t, "1.0.2", out.Version)
 | 
						|
	assert.Equal(t, "new", out.Hostname)
 | 
						|
 | 
						|
	// Make sure original base spec not changed
 | 
						|
	assert.NotEqual(t, out, c.baseOCISpecs["/etc/containerd/cri-base.json"])
 | 
						|
	assert.Equal(t, c.baseOCISpecs["/etc/containerd/cri-base.json"].Hostname, "old")
 | 
						|
 | 
						|
	assert.Equal(t, filepath.Join("/", constants.K8sContainerdNamespace, "id1"), out.Linux.CgroupsPath)
 | 
						|
}
 | 
						|
 | 
						|
func TestRuntimeSnapshotter(t *testing.T) {
 | 
						|
	defaultRuntime := config.Runtime{
 | 
						|
		Snapshotter: "",
 | 
						|
	}
 | 
						|
 | 
						|
	fooRuntime := config.Runtime{
 | 
						|
		Snapshotter: "devmapper",
 | 
						|
	}
 | 
						|
 | 
						|
	for desc, test := range map[string]struct {
 | 
						|
		runtime           config.Runtime
 | 
						|
		expectSnapshotter string
 | 
						|
	}{
 | 
						|
		"should return default snapshotter when runtime.Snapshotter is not set": {
 | 
						|
			runtime:           defaultRuntime,
 | 
						|
			expectSnapshotter: config.DefaultConfig().Snapshotter,
 | 
						|
		},
 | 
						|
		"should return overridden snapshotter when runtime.Snapshotter is set": {
 | 
						|
			runtime:           fooRuntime,
 | 
						|
			expectSnapshotter: "devmapper",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(desc, func(t *testing.T) {
 | 
						|
			cri := newTestCRIService()
 | 
						|
			cri.config = config.Config{
 | 
						|
				PluginConfig: config.DefaultConfig(),
 | 
						|
			}
 | 
						|
			assert.Equal(t, test.expectSnapshotter, cri.runtimeSnapshotter(context.Background(), test.runtime))
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |