github.com/containerd/Containerd@v1.4.13/oci/spec_opts_test.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package oci
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"log"
    27  	"os"
    28  	"reflect"
    29  	"runtime"
    30  	"strings"
    31  	"testing"
    32  
    33  	"github.com/containerd/containerd/content"
    34  	"github.com/opencontainers/go-digest"
    35  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    36  
    37  	"github.com/containerd/containerd/containers"
    38  	"github.com/containerd/containerd/namespaces"
    39  	"github.com/opencontainers/runtime-spec/specs-go"
    40  )
    41  
    42  type blob []byte
    43  
    44  func (b blob) ReadAt(p []byte, off int64) (int, error) {
    45  	if off >= int64(len(b)) {
    46  		return 0, io.EOF
    47  	}
    48  	return copy(p, b[off:]), nil
    49  }
    50  
    51  func (b blob) Close() error {
    52  	return nil
    53  }
    54  
    55  func (b blob) Size() int64 {
    56  	return int64(len(b))
    57  }
    58  
    59  type fakeImage struct {
    60  	config ocispec.Descriptor
    61  	blobs  map[string]blob
    62  }
    63  
    64  func newFakeImage(config ocispec.Image) (Image, error) {
    65  	configBlob, err := json.Marshal(config)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	configDescriptor := ocispec.Descriptor{
    70  		MediaType: ocispec.MediaTypeImageConfig,
    71  		Digest:    digest.NewDigestFromBytes(digest.SHA256, configBlob),
    72  	}
    73  
    74  	return fakeImage{
    75  		config: configDescriptor,
    76  		blobs: map[string]blob{
    77  			configDescriptor.Digest.String(): configBlob,
    78  		},
    79  	}, nil
    80  }
    81  
    82  func (i fakeImage) Config(ctx context.Context) (ocispec.Descriptor, error) {
    83  	return i.config, nil
    84  }
    85  
    86  func (i fakeImage) ContentStore() content.Store {
    87  	return i
    88  }
    89  
    90  func (i fakeImage) ReaderAt(ctx context.Context, dec ocispec.Descriptor) (content.ReaderAt, error) {
    91  	blob, found := i.blobs[dec.Digest.String()]
    92  	if !found {
    93  		return nil, errors.New("not found")
    94  	}
    95  	return blob, nil
    96  }
    97  
    98  func (i fakeImage) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
    99  	return content.Info{}, errors.New("not implemented")
   100  }
   101  
   102  func (i fakeImage) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   103  	return content.Info{}, errors.New("not implemented")
   104  }
   105  
   106  func (i fakeImage) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   107  	return errors.New("not implemented")
   108  }
   109  
   110  func (i fakeImage) Delete(ctx context.Context, dgst digest.Digest) error {
   111  	return errors.New("not implemented")
   112  }
   113  
   114  func (i fakeImage) Status(ctx context.Context, ref string) (content.Status, error) {
   115  	return content.Status{}, errors.New("not implemented")
   116  }
   117  
   118  func (i fakeImage) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
   119  	return nil, errors.New("not implemented")
   120  }
   121  
   122  func (i fakeImage) Abort(ctx context.Context, ref string) error {
   123  	return errors.New("not implemented")
   124  }
   125  
   126  func (i fakeImage) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   127  	return nil, errors.New("not implemented")
   128  }
   129  
   130  func TestReplaceOrAppendEnvValues(t *testing.T) {
   131  	t.Parallel()
   132  
   133  	defaults := []string{
   134  		"o=ups", "p=$e", "x=foo", "y=boo", "z", "t=",
   135  	}
   136  	overrides := []string{
   137  		"x=bar", "y", "a=42", "o=", "e", "s=",
   138  	}
   139  	expected := []string{
   140  		"o=", "p=$e", "x=bar", "z", "t=", "a=42", "s=",
   141  	}
   142  
   143  	defaultsOrig := make([]string, len(defaults))
   144  	copy(defaultsOrig, defaults)
   145  	overridesOrig := make([]string, len(overrides))
   146  	copy(overridesOrig, overrides)
   147  
   148  	results := replaceOrAppendEnvValues(defaults, overrides)
   149  
   150  	if err := assertEqualsStringArrays(defaults, defaultsOrig); err != nil {
   151  		t.Fatal(err)
   152  	}
   153  	if err := assertEqualsStringArrays(overrides, overridesOrig); err != nil {
   154  		t.Fatal(err)
   155  	}
   156  
   157  	if err := assertEqualsStringArrays(results, expected); err != nil {
   158  		t.Fatal(err)
   159  	}
   160  }
   161  
   162  func TestWithDefaultSpecForPlatform(t *testing.T) {
   163  	t.Parallel()
   164  	var (
   165  		s   Spec
   166  		c   = containers.Container{ID: "TestWithDefaultSpecForPlatform"}
   167  		ctx = namespaces.WithNamespace(context.Background(), "test")
   168  	)
   169  
   170  	platforms := []string{"linux/amd64", "windows/amd64"}
   171  	for _, p := range platforms {
   172  		if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpecForPlatform(p)); err != nil {
   173  			t.Fatal(err)
   174  		}
   175  	}
   176  
   177  }
   178  
   179  func Contains(a []string, x string) bool {
   180  	for _, n := range a {
   181  		if x == n {
   182  			return true
   183  		}
   184  	}
   185  	return false
   186  }
   187  
   188  func TestWithDefaultPathEnv(t *testing.T) {
   189  	t.Parallel()
   190  	s := Spec{}
   191  	s.Process = &specs.Process{
   192  		Env: []string{},
   193  	}
   194  	var (
   195  		defaultUnixEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
   196  		ctx            = namespaces.WithNamespace(context.Background(), "test")
   197  	)
   198  	WithDefaultPathEnv(ctx, nil, nil, &s)
   199  	if !Contains(s.Process.Env, defaultUnixEnv) {
   200  		t.Fatal("default Unix Env not found")
   201  	}
   202  }
   203  
   204  func TestWithProcessCwd(t *testing.T) {
   205  	t.Parallel()
   206  	s := Spec{}
   207  	opts := []SpecOpts{
   208  		WithProcessCwd("testCwd"),
   209  	}
   210  	var expectedCwd = "testCwd"
   211  
   212  	for _, opt := range opts {
   213  		if err := opt(nil, nil, nil, &s); err != nil {
   214  			t.Fatal(err)
   215  		}
   216  	}
   217  	if s.Process.Cwd != expectedCwd {
   218  		t.Fatal("Process has a wrong current working directory")
   219  	}
   220  
   221  }
   222  
   223  func TestWithEnv(t *testing.T) {
   224  	t.Parallel()
   225  
   226  	s := Spec{}
   227  	s.Process = &specs.Process{
   228  		Env: []string{"DEFAULT=test"},
   229  	}
   230  
   231  	WithEnv([]string{"env=1"})(context.Background(), nil, nil, &s)
   232  
   233  	if len(s.Process.Env) != 2 {
   234  		t.Fatal("didn't append")
   235  	}
   236  
   237  	WithEnv([]string{"env2=1"})(context.Background(), nil, nil, &s)
   238  
   239  	if len(s.Process.Env) != 3 {
   240  		t.Fatal("didn't append")
   241  	}
   242  
   243  	WithEnv([]string{"env2=2"})(context.Background(), nil, nil, &s)
   244  
   245  	if s.Process.Env[2] != "env2=2" {
   246  		t.Fatal("couldn't update")
   247  	}
   248  
   249  	WithEnv([]string{"env2"})(context.Background(), nil, nil, &s)
   250  
   251  	if len(s.Process.Env) != 2 {
   252  		t.Fatal("couldn't unset")
   253  	}
   254  }
   255  
   256  func TestWithMounts(t *testing.T) {
   257  
   258  	t.Parallel()
   259  
   260  	s := Spec{
   261  		Mounts: []specs.Mount{
   262  			{
   263  				Source:      "default-source",
   264  				Destination: "default-dest",
   265  			},
   266  		},
   267  	}
   268  
   269  	WithMounts([]specs.Mount{
   270  		{
   271  			Source:      "new-source",
   272  			Destination: "new-dest",
   273  		},
   274  	})(nil, nil, nil, &s)
   275  
   276  	if len(s.Mounts) != 2 {
   277  		t.Fatal("didn't append")
   278  	}
   279  
   280  	if s.Mounts[1].Source != "new-source" {
   281  		t.Fatal("invalid mount")
   282  	}
   283  
   284  	if s.Mounts[1].Destination != "new-dest" {
   285  		t.Fatal("invalid mount")
   286  	}
   287  }
   288  
   289  func TestWithDefaultSpec(t *testing.T) {
   290  	t.Parallel()
   291  	var (
   292  		s   Spec
   293  		c   = containers.Container{ID: "TestWithDefaultSpec"}
   294  		ctx = namespaces.WithNamespace(context.Background(), "test")
   295  	)
   296  
   297  	if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpec()); err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	var expected Spec
   302  	var err error
   303  	if runtime.GOOS == "windows" {
   304  		err = populateDefaultWindowsSpec(ctx, &expected, c.ID)
   305  	} else {
   306  		err = populateDefaultUnixSpec(ctx, &expected, c.ID)
   307  	}
   308  	if err != nil {
   309  		t.Fatal(err)
   310  	}
   311  
   312  	if reflect.DeepEqual(s, Spec{}) {
   313  		t.Fatalf("spec should not be empty")
   314  	}
   315  
   316  	if !reflect.DeepEqual(&s, &expected) {
   317  		t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected)
   318  	}
   319  }
   320  
   321  func TestWithSpecFromFile(t *testing.T) {
   322  	t.Parallel()
   323  	var (
   324  		s   Spec
   325  		c   = containers.Container{ID: "TestWithDefaultSpec"}
   326  		ctx = namespaces.WithNamespace(context.Background(), "test")
   327  	)
   328  
   329  	fp, err := ioutil.TempFile("", "testwithdefaultspec.json")
   330  	if err != nil {
   331  		t.Fatal(err)
   332  	}
   333  	defer func() {
   334  		if err := os.Remove(fp.Name()); err != nil {
   335  			log.Printf("failed to remove tempfile %v: %v", fp.Name(), err)
   336  		}
   337  	}()
   338  	defer fp.Close()
   339  
   340  	expected, err := GenerateSpec(ctx, nil, &c)
   341  	if err != nil {
   342  		t.Fatal(err)
   343  	}
   344  
   345  	p, err := json.Marshal(expected)
   346  	if err != nil {
   347  		t.Fatal(err)
   348  	}
   349  
   350  	if _, err := fp.Write(p); err != nil {
   351  		t.Fatal(err)
   352  	}
   353  
   354  	if err := ApplyOpts(ctx, nil, &c, &s, WithSpecFromFile(fp.Name())); err != nil {
   355  		t.Fatal(err)
   356  	}
   357  
   358  	if reflect.DeepEqual(s, Spec{}) {
   359  		t.Fatalf("spec should not be empty")
   360  	}
   361  
   362  	if !reflect.DeepEqual(&s, expected) {
   363  		t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected)
   364  	}
   365  }
   366  
   367  func TestWithMemoryLimit(t *testing.T) {
   368  	var (
   369  		ctx = namespaces.WithNamespace(context.Background(), "testing")
   370  		c   = containers.Container{ID: t.Name()}
   371  		m   = uint64(768 * 1024 * 1024)
   372  		o   = WithMemoryLimit(m)
   373  	)
   374  	// Test with all three supported scenarios
   375  	platforms := []string{"", "linux/amd64", "windows/amd64"}
   376  	for _, p := range platforms {
   377  		var spec *Spec
   378  		var err error
   379  		if p == "" {
   380  			t.Log("Testing GenerateSpec default platform")
   381  			spec, err = GenerateSpec(ctx, nil, &c, o)
   382  
   383  			// Convert the platform to the default based on GOOS like
   384  			// GenerateSpec does.
   385  			switch runtime.GOOS {
   386  			case "linux":
   387  				p = "linux/amd64"
   388  			case "windows":
   389  				p = "windows/amd64"
   390  			}
   391  		} else {
   392  			t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
   393  			spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
   394  		}
   395  		if err != nil {
   396  			t.Fatalf("failed to generate spec with: %v", err)
   397  		}
   398  		switch p {
   399  		case "linux/amd64":
   400  			if *spec.Linux.Resources.Memory.Limit != int64(m) {
   401  				t.Fatalf("spec.Linux.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Linux.Resources.Memory.Limit)
   402  			}
   403  			// If we are linux/amd64 on Windows GOOS it is LCOW
   404  			if runtime.GOOS == "windows" {
   405  				// Verify that we also set the Windows section.
   406  				if *spec.Windows.Resources.Memory.Limit != m {
   407  					t.Fatalf("for LCOW spec.Windows.Resources.Memory.Limit is also expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit)
   408  				}
   409  			} else {
   410  				if spec.Windows != nil {
   411  					t.Fatalf("spec.Windows section should not be set for linux/amd64 spec on non-windows platform")
   412  				}
   413  			}
   414  		case "windows/amd64":
   415  			if *spec.Windows.Resources.Memory.Limit != m {
   416  				t.Fatalf("spec.Windows.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit)
   417  			}
   418  			if spec.Linux != nil {
   419  				t.Fatalf("spec.Linux section should not be set for windows/amd64 spec ever")
   420  			}
   421  		}
   422  	}
   423  }
   424  
   425  func isEqualStringArrays(values, expected []string) bool {
   426  	if len(values) != len(expected) {
   427  		return false
   428  	}
   429  
   430  	for i, x := range expected {
   431  		if values[i] != x {
   432  			return false
   433  		}
   434  	}
   435  	return true
   436  }
   437  
   438  func assertEqualsStringArrays(values, expected []string) error {
   439  	if !isEqualStringArrays(values, expected) {
   440  		return fmt.Errorf("expected %s, but found %s", expected, values)
   441  	}
   442  	return nil
   443  }
   444  
   445  func TestWithTTYSize(t *testing.T) {
   446  	t.Parallel()
   447  	s := Spec{}
   448  	opts := []SpecOpts{
   449  		WithTTYSize(10, 20),
   450  	}
   451  	var (
   452  		expectedWidth  = uint(10)
   453  		expectedHeight = uint(20)
   454  	)
   455  
   456  	for _, opt := range opts {
   457  		if err := opt(nil, nil, nil, &s); err != nil {
   458  			t.Fatal(err)
   459  		}
   460  	}
   461  	if s.Process.ConsoleSize.Height != expectedWidth && s.Process.ConsoleSize.Height != expectedHeight {
   462  		t.Fatal("Process Console has invalid size")
   463  	}
   464  
   465  }
   466  
   467  func TestWithUserNamespace(t *testing.T) {
   468  	t.Parallel()
   469  	s := Spec{}
   470  
   471  	opts := []SpecOpts{
   472  		WithUserNamespace([]specs.LinuxIDMapping{
   473  			{
   474  				ContainerID: 1,
   475  				HostID:      2,
   476  				Size:        10000,
   477  			},
   478  		}, []specs.LinuxIDMapping{
   479  			{
   480  				ContainerID: 2,
   481  				HostID:      3,
   482  				Size:        20000,
   483  			},
   484  		}),
   485  	}
   486  
   487  	for _, opt := range opts {
   488  		if err := opt(nil, nil, nil, &s); err != nil {
   489  			t.Fatal(err)
   490  		}
   491  	}
   492  
   493  	expectedUIDMapping := specs.LinuxIDMapping{
   494  		ContainerID: 1,
   495  		HostID:      2,
   496  		Size:        10000,
   497  	}
   498  	expectedGIDMapping := specs.LinuxIDMapping{
   499  		ContainerID: 2,
   500  		HostID:      3,
   501  		Size:        20000,
   502  	}
   503  
   504  	if !(len(s.Linux.UIDMappings) == 1 && s.Linux.UIDMappings[0] == expectedUIDMapping) || !(len(s.Linux.GIDMappings) == 1 && s.Linux.GIDMappings[0] == expectedGIDMapping) {
   505  		t.Fatal("WithUserNamespace Cannot set the uid/gid mappings for the task")
   506  	}
   507  
   508  }
   509  func TestWithImageConfigArgs(t *testing.T) {
   510  	t.Parallel()
   511  
   512  	img, err := newFakeImage(ocispec.Image{
   513  		Config: ocispec.ImageConfig{
   514  			Env:        []string{"z=bar", "y=baz"},
   515  			Entrypoint: []string{"create", "--namespace=test"},
   516  			Cmd:        []string{"", "--debug"},
   517  		},
   518  	})
   519  	if err != nil {
   520  		t.Fatal(err)
   521  	}
   522  
   523  	s := Spec{
   524  		Version: specs.Version,
   525  		Root:    &specs.Root{},
   526  		Windows: &specs.Windows{},
   527  	}
   528  
   529  	opts := []SpecOpts{
   530  		WithEnv([]string{"x=foo", "y=boo"}),
   531  		WithProcessArgs("run", "--foo", "xyz", "--bar"),
   532  		WithImageConfigArgs(img, []string{"--boo", "bar"}),
   533  	}
   534  
   535  	expectedEnv := []string{"z=bar", "y=boo", "x=foo"}
   536  	expectedArgs := []string{"create", "--namespace=test", "--boo", "bar"}
   537  
   538  	for _, opt := range opts {
   539  		if err := opt(nil, nil, nil, &s); err != nil {
   540  			t.Fatal(err)
   541  		}
   542  	}
   543  
   544  	if err := assertEqualsStringArrays(s.Process.Env, expectedEnv); err != nil {
   545  		t.Fatal(err)
   546  	}
   547  	if err := assertEqualsStringArrays(s.Process.Args, expectedArgs); err != nil {
   548  		t.Fatal(err)
   549  	}
   550  }
   551  
   552  func TestAddCaps(t *testing.T) {
   553  	t.Parallel()
   554  
   555  	var s specs.Spec
   556  
   557  	if err := WithAddedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
   558  		t.Fatal(err)
   559  	}
   560  	for i, cl := range [][]string{
   561  		s.Process.Capabilities.Bounding,
   562  		s.Process.Capabilities.Effective,
   563  		s.Process.Capabilities.Permitted,
   564  		s.Process.Capabilities.Inheritable,
   565  	} {
   566  		if !capsContain(cl, "CAP_CHOWN") {
   567  			t.Errorf("cap list %d does not contain added cap", i)
   568  		}
   569  	}
   570  }
   571  
   572  func TestDropCaps(t *testing.T) {
   573  	t.Parallel()
   574  
   575  	var s specs.Spec
   576  
   577  	if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil {
   578  		t.Fatal(err)
   579  	}
   580  	if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
   581  		t.Fatal(err)
   582  	}
   583  
   584  	for i, cl := range [][]string{
   585  		s.Process.Capabilities.Bounding,
   586  		s.Process.Capabilities.Effective,
   587  		s.Process.Capabilities.Permitted,
   588  		s.Process.Capabilities.Inheritable,
   589  	} {
   590  		if capsContain(cl, "CAP_CHOWN") {
   591  			t.Errorf("cap list %d contains dropped cap", i)
   592  		}
   593  	}
   594  
   595  	// Add all capabilities back and drop a different cap.
   596  	if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil {
   597  		t.Fatal(err)
   598  	}
   599  	if err := WithDroppedCapabilities([]string{"CAP_FOWNER"})(context.Background(), nil, nil, &s); err != nil {
   600  		t.Fatal(err)
   601  	}
   602  
   603  	for i, cl := range [][]string{
   604  		s.Process.Capabilities.Bounding,
   605  		s.Process.Capabilities.Effective,
   606  		s.Process.Capabilities.Permitted,
   607  		s.Process.Capabilities.Inheritable,
   608  	} {
   609  		if capsContain(cl, "CAP_FOWNER") {
   610  			t.Errorf("cap list %d contains dropped cap", i)
   611  		}
   612  		if !capsContain(cl, "CAP_CHOWN") {
   613  			t.Errorf("cap list %d doesn't contain non-dropped cap", i)
   614  		}
   615  	}
   616  
   617  	// Drop all duplicated caps.
   618  	if err := WithCapabilities([]string{"CAP_CHOWN", "CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
   619  		t.Fatal(err)
   620  	}
   621  	if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
   622  		t.Fatal(err)
   623  	}
   624  	for i, cl := range [][]string{
   625  		s.Process.Capabilities.Bounding,
   626  		s.Process.Capabilities.Effective,
   627  		s.Process.Capabilities.Permitted,
   628  		s.Process.Capabilities.Inheritable,
   629  	} {
   630  		if len(cl) != 0 {
   631  			t.Errorf("cap list %d is not empty", i)
   632  		}
   633  	}
   634  }
   635  
   636  func TestDevShmSize(t *testing.T) {
   637  	t.Parallel()
   638  	var (
   639  		s   Spec
   640  		c   = containers.Container{ID: t.Name()}
   641  		ctx = namespaces.WithNamespace(context.Background(), "test")
   642  	)
   643  
   644  	err := populateDefaultUnixSpec(ctx, &s, c.ID)
   645  	if err != nil {
   646  		t.Fatal(err)
   647  	}
   648  
   649  	expected := "1024k"
   650  	if err := WithDevShmSize(1024)(nil, nil, nil, &s); err != nil {
   651  		t.Fatal(err)
   652  	}
   653  	m := getShmMount(&s)
   654  	if m == nil {
   655  		t.Fatal("no shm mount found")
   656  	}
   657  	o := getShmSize(m.Options)
   658  	if o == "" {
   659  		t.Fatal("shm size not specified")
   660  	}
   661  	parts := strings.Split(o, "=")
   662  	if len(parts) != 2 {
   663  		t.Fatal("invalid size format")
   664  	}
   665  	size := parts[1]
   666  	if size != expected {
   667  		t.Fatalf("size %s not equal %s", size, expected)
   668  	}
   669  }
   670  
   671  func getShmMount(s *Spec) *specs.Mount {
   672  	for _, m := range s.Mounts {
   673  		if m.Source == "shm" && m.Type == "tmpfs" {
   674  			return &m
   675  		}
   676  	}
   677  	return nil
   678  }
   679  
   680  func getShmSize(opts []string) string {
   681  	for _, o := range opts {
   682  		if strings.HasPrefix(o, "size=") {
   683  			return o
   684  		}
   685  	}
   686  	return ""
   687  }