github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/resource/deploy_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resource
     5  
     6  import (
     7  	"bytes"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/juju/core/resources"
    15  	"github.com/juju/testing"
    16  	jc "github.com/juju/testing/checkers"
    17  	gc "gopkg.in/check.v1"
    18  	"gopkg.in/juju/charm.v6"
    19  	charmresource "gopkg.in/juju/charm.v6/resource"
    20  	"gopkg.in/macaroon.v2-unstable"
    21  
    22  	"github.com/juju/juju/charmstore"
    23  )
    24  
    25  type DeploySuite struct {
    26  	testing.IsolationSuite
    27  
    28  	stub *testing.Stub
    29  }
    30  
    31  var _ = gc.Suite(&DeploySuite{})
    32  
    33  func (s *DeploySuite) SetUpTest(c *gc.C) {
    34  	s.IsolationSuite.SetUpTest(c)
    35  
    36  	s.stub = &testing.Stub{}
    37  }
    38  
    39  func (s DeploySuite) TestDeployResourcesWithoutFiles(c *gc.C) {
    40  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
    41  	cURL := charm.MustParseURL("cs:~a-user/trusty/spam-5")
    42  	chID := charmstore.CharmID{
    43  		URL: cURL,
    44  	}
    45  	csMac := &macaroon.Macaroon{}
    46  	resources := map[string]charmresource.Meta{
    47  		"store-tarball": {
    48  			Name: "store-tarball",
    49  			Type: charmresource.TypeFile,
    50  			Path: "store.tgz",
    51  		},
    52  		"store-zip": {
    53  			Name: "store-zip",
    54  			Type: charmresource.TypeFile,
    55  			Path: "store.zip",
    56  		},
    57  	}
    58  
    59  	ids, err := DeployResources(DeployResourcesArgs{
    60  		ApplicationID:      "mysql",
    61  		CharmID:            chID,
    62  		CharmStoreMacaroon: csMac,
    63  		ResourceValues:     nil,
    64  		Client:             deps,
    65  		ResourcesMeta:      resources,
    66  	})
    67  	c.Assert(err, jc.ErrorIsNil)
    68  
    69  	c.Check(ids, gc.DeepEquals, map[string]string{
    70  		"store-tarball": "id-store-tarball",
    71  		"store-zip":     "id-store-zip",
    72  	})
    73  
    74  	s.stub.CheckCallNames(c, "AddPendingResources")
    75  	s.stub.CheckCall(c, 0, "AddPendingResources", "mysql", chID, csMac, []charmresource.Resource{{
    76  		Meta:     resources["store-tarball"],
    77  		Origin:   charmresource.OriginStore,
    78  		Revision: -1,
    79  	}, {
    80  		Meta:     resources["store-zip"],
    81  		Origin:   charmresource.OriginStore,
    82  		Revision: -1,
    83  	}})
    84  }
    85  
    86  func (s DeploySuite) TestUploadFilesOnly(c *gc.C) {
    87  	deps := uploadDeps{s.stub, rsc{bytes.NewBufferString("file contents")}}
    88  	cURL := charm.MustParseURL("cs:~a-user/trusty/spam-5")
    89  	chID := charmstore.CharmID{
    90  		URL: cURL,
    91  	}
    92  	csMac := &macaroon.Macaroon{}
    93  	du := deployUploader{
    94  		applicationID: "mysql",
    95  		chID:          chID,
    96  		csMac:         csMac,
    97  		client:        deps,
    98  		resources: map[string]charmresource.Meta{
    99  			"upload": {
   100  				Name: "upload",
   101  				Type: charmresource.TypeFile,
   102  				Path: "upload",
   103  			},
   104  			"store": {
   105  				Name: "store",
   106  				Type: charmresource.TypeFile,
   107  				Path: "store",
   108  			},
   109  		},
   110  		osOpen: deps.Open,
   111  		osStat: deps.Stat,
   112  	}
   113  
   114  	files := map[string]string{
   115  		"upload": "foobar.txt",
   116  	}
   117  	revisions := map[string]int{}
   118  	ids, err := du.upload(files, revisions)
   119  	c.Assert(err, jc.ErrorIsNil)
   120  	c.Check(ids, gc.DeepEquals, map[string]string{
   121  		"upload": "id-upload",
   122  		"store":  "id-store",
   123  	})
   124  
   125  	s.stub.CheckCallNames(c, "Stat", "AddPendingResources", "Open", "UploadPendingResource")
   126  	expectedStore := []charmresource.Resource{
   127  		{
   128  			Meta:     du.resources["store"],
   129  			Origin:   charmresource.OriginStore,
   130  			Revision: -1,
   131  		},
   132  	}
   133  	s.stub.CheckCall(c, 1, "AddPendingResources", "mysql", chID, csMac, expectedStore)
   134  	s.stub.CheckCall(c, 2, "Open", "foobar.txt")
   135  
   136  	expectedUpload := charmresource.Resource{
   137  		Meta:   du.resources["upload"],
   138  		Origin: charmresource.OriginUpload,
   139  	}
   140  	s.stub.CheckCall(c, 3, "UploadPendingResource", "mysql", expectedUpload, "foobar.txt", "file contents")
   141  }
   142  
   143  func (s DeploySuite) TestUploadRevisionsOnly(c *gc.C) {
   144  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   145  	cURL := charm.MustParseURL("cs:~a-user/trusty/spam-5")
   146  	chID := charmstore.CharmID{
   147  		URL: cURL,
   148  	}
   149  	csMac := &macaroon.Macaroon{}
   150  	du := deployUploader{
   151  		applicationID: "mysql",
   152  		chID:          chID,
   153  		csMac:         csMac,
   154  		client:        deps,
   155  		resources: map[string]charmresource.Meta{
   156  			"upload": {
   157  				Name: "upload",
   158  				Type: charmresource.TypeFile,
   159  				Path: "upload",
   160  			},
   161  			"store": {
   162  				Name: "store",
   163  				Type: charmresource.TypeFile,
   164  				Path: "store",
   165  			},
   166  		},
   167  		osOpen: deps.Open,
   168  		osStat: deps.Stat,
   169  	}
   170  
   171  	files := map[string]string{}
   172  	revisions := map[string]int{
   173  		"store": 3,
   174  	}
   175  	ids, err := du.upload(files, revisions)
   176  	c.Assert(err, jc.ErrorIsNil)
   177  	c.Check(ids, gc.DeepEquals, map[string]string{
   178  		"upload": "id-upload",
   179  		"store":  "id-store",
   180  	})
   181  
   182  	s.stub.CheckCallNames(c, "AddPendingResources")
   183  	expectedStore := []charmresource.Resource{{
   184  		Meta:     du.resources["store"],
   185  		Origin:   charmresource.OriginStore,
   186  		Revision: 3,
   187  	}, {
   188  		Meta:     du.resources["upload"],
   189  		Origin:   charmresource.OriginStore,
   190  		Revision: -1,
   191  	}}
   192  	s.stub.CheckCall(c, 0, "AddPendingResources", "mysql", chID, csMac, expectedStore)
   193  }
   194  
   195  func (s DeploySuite) TestUploadFilesAndRevisions(c *gc.C) {
   196  	deps := uploadDeps{s.stub, rsc{bytes.NewBufferString("file contents")}}
   197  	cURL := charm.MustParseURL("cs:~a-user/trusty/spam-5")
   198  	chID := charmstore.CharmID{
   199  		URL: cURL,
   200  	}
   201  	csMac := &macaroon.Macaroon{}
   202  	du := deployUploader{
   203  		applicationID: "mysql",
   204  		chID:          chID,
   205  		csMac:         csMac,
   206  		client:        deps,
   207  		resources: map[string]charmresource.Meta{
   208  			"upload": {
   209  				Name: "upload",
   210  				Type: charmresource.TypeFile,
   211  				Path: "upload",
   212  			},
   213  			"store": {
   214  				Name: "store",
   215  				Type: charmresource.TypeFile,
   216  				Path: "store",
   217  			},
   218  		},
   219  		osOpen: deps.Open,
   220  		osStat: deps.Stat,
   221  	}
   222  
   223  	files := map[string]string{
   224  		"upload": "foobar.txt",
   225  	}
   226  	revisions := map[string]int{
   227  		"store": 3,
   228  	}
   229  	ids, err := du.upload(files, revisions)
   230  	c.Assert(err, jc.ErrorIsNil)
   231  	c.Check(ids, gc.DeepEquals, map[string]string{
   232  		"upload": "id-upload",
   233  		"store":  "id-store",
   234  	})
   235  
   236  	s.stub.CheckCallNames(c, "Stat", "AddPendingResources", "Open", "UploadPendingResource")
   237  	expectedStore := []charmresource.Resource{
   238  		{
   239  			Meta:     du.resources["store"],
   240  			Origin:   charmresource.OriginStore,
   241  			Revision: 3,
   242  		},
   243  	}
   244  	s.stub.CheckCall(c, 1, "AddPendingResources", "mysql", chID, csMac, expectedStore)
   245  	s.stub.CheckCall(c, 2, "Open", "foobar.txt")
   246  
   247  	expectedUpload := charmresource.Resource{
   248  		Meta:   du.resources["upload"],
   249  		Origin: charmresource.OriginUpload,
   250  	}
   251  	s.stub.CheckCall(c, 3, "UploadPendingResource", "mysql", expectedUpload, "foobar.txt", "file contents")
   252  }
   253  
   254  func (s DeploySuite) TestUploadUnexpectedResourceFile(c *gc.C) {
   255  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   256  	du := deployUploader{
   257  		applicationID: "mysql",
   258  		client:        deps,
   259  		resources: map[string]charmresource.Meta{
   260  			"res1": {
   261  				Name: "res1",
   262  				Type: charmresource.TypeFile,
   263  				Path: "path",
   264  			},
   265  		},
   266  		osOpen: deps.Open,
   267  		osStat: deps.Stat,
   268  	}
   269  
   270  	files := map[string]string{"some bad resource": "foobar.txt"}
   271  	revisions := map[string]int{}
   272  	_, err := du.upload(files, revisions)
   273  	c.Check(err, gc.ErrorMatches, `unrecognized resource "some bad resource"`)
   274  
   275  	s.stub.CheckNoCalls(c)
   276  }
   277  
   278  func (s DeploySuite) TestUploadUnexpectedResourceRevision(c *gc.C) {
   279  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   280  	du := deployUploader{
   281  		applicationID: "mysql",
   282  		client:        deps,
   283  		resources: map[string]charmresource.Meta{
   284  			"res1": {
   285  				Name: "res1",
   286  				Type: charmresource.TypeFile,
   287  				Path: "path",
   288  			},
   289  		},
   290  		osOpen: deps.Open,
   291  		osStat: deps.Stat,
   292  	}
   293  
   294  	files := map[string]string{}
   295  	revisions := map[string]int{"some bad resource": 2}
   296  	_, err := du.upload(files, revisions)
   297  	c.Check(err, gc.ErrorMatches, `unrecognized resource "some bad resource"`)
   298  
   299  	s.stub.CheckNoCalls(c)
   300  }
   301  
   302  func (s DeploySuite) TestMissingResource(c *gc.C) {
   303  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   304  	du := deployUploader{
   305  		applicationID: "mysql",
   306  		client:        deps,
   307  		resources: map[string]charmresource.Meta{
   308  			"res1": {
   309  				Name: "res1",
   310  				Type: charmresource.TypeFile,
   311  				Path: "path",
   312  			},
   313  		},
   314  		osOpen: deps.Open,
   315  		osStat: deps.Stat,
   316  	}
   317  
   318  	// set the error that will be returned by os.Stat
   319  	s.stub.SetErrors(os.ErrNotExist)
   320  
   321  	files := map[string]string{"res1": "foobar.txt"}
   322  	revisions := map[string]int{}
   323  	_, err := du.upload(files, revisions)
   324  	c.Check(err, gc.ErrorMatches, `file for resource "res1".*`)
   325  	c.Check(errors.Cause(err), jc.Satisfies, os.IsNotExist)
   326  }
   327  
   328  func (s DeploySuite) TestDeployDockerResourceRegistryPathString(c *gc.C) {
   329  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   330  	cURL := charm.MustParseURL("cs:~a-user/mysql-k8s-5")
   331  	chID := charmstore.CharmID{
   332  		URL: cURL,
   333  	}
   334  	csMac := &macaroon.Macaroon{}
   335  	resourceMeta := map[string]charmresource.Meta{
   336  		"mysql_image": {
   337  			Name: "mysql_image",
   338  			Type: charmresource.TypeContainerImage,
   339  		},
   340  	}
   341  
   342  	passedResourceValues := map[string]string{
   343  		"mysql_image": "mariadb:10.3.8",
   344  	}
   345  
   346  	du := deployUploader{
   347  		applicationID: "mysql",
   348  		chID:          chID,
   349  		csMac:         csMac,
   350  		client:        deps,
   351  		resources:     resourceMeta,
   352  		osOpen:        deps.Open,
   353  		osStat:        deps.Stat,
   354  	}
   355  	ids, err := du.upload(passedResourceValues, map[string]int{})
   356  	c.Assert(err, jc.ErrorIsNil)
   357  	c.Check(ids, gc.DeepEquals, map[string]string{
   358  		"mysql_image": "id-mysql_image",
   359  	})
   360  
   361  	expectedUpload := charmresource.Resource{
   362  		Meta:   resourceMeta["mysql_image"],
   363  		Origin: charmresource.OriginUpload,
   364  	}
   365  
   366  	expectedUploadData := "{\"ImageName\":\"mariadb:10.3.8\",\"Username\":\"\"}"
   367  	s.stub.CheckCallNames(c, "UploadPendingResource")
   368  	s.stub.CheckCall(c, 0, "UploadPendingResource", "mysql", expectedUpload, "mariadb:10.3.8", expectedUploadData)
   369  }
   370  
   371  func (s DeploySuite) TestDeployDockerResourceJSONFile(c *gc.C) {
   372  	fileContents := `
   373  {
   374    "ImageName": "registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image",
   375    "Username": "docker-registry",
   376    "Password": "hunter2"
   377  }
   378  `
   379  	dir := c.MkDir()
   380  	jsonFile := path.Join(dir, "details.json")
   381  	err := ioutil.WriteFile(jsonFile, []byte(fileContents), 0600)
   382  	c.Assert(err, jc.ErrorIsNil)
   383  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   384  	cURL := charm.MustParseURL("cs:~a-user/mysql-k8s-5")
   385  	chID := charmstore.CharmID{
   386  		URL: cURL,
   387  	}
   388  	csMac := &macaroon.Macaroon{}
   389  	resourceMeta := map[string]charmresource.Meta{
   390  		"mysql_image": {
   391  			Name: "mysql_image",
   392  			Type: charmresource.TypeContainerImage,
   393  		},
   394  	}
   395  
   396  	passedResourceValues := map[string]string{
   397  		"mysql_image": jsonFile,
   398  	}
   399  	du := deployUploader{
   400  		applicationID: "mysql",
   401  		chID:          chID,
   402  		csMac:         csMac,
   403  		client:        deps,
   404  		resources:     resourceMeta,
   405  		osOpen:        deps.Open,
   406  		osStat:        deps.Stat,
   407  	}
   408  	ids, err := du.upload(passedResourceValues, map[string]int{})
   409  	c.Assert(err, jc.ErrorIsNil)
   410  	c.Check(ids, gc.DeepEquals, map[string]string{
   411  		"mysql_image": "id-mysql_image",
   412  	})
   413  
   414  	expectedUpload := charmresource.Resource{
   415  		Meta:   resourceMeta["mysql_image"],
   416  		Origin: charmresource.OriginUpload,
   417  	}
   418  
   419  	expectedUploadData := "{\"ImageName\":\"registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image\",\"Username\":\"docker-registry\",\"Password\":\"hunter2\"}"
   420  	s.stub.CheckCallNames(c, "UploadPendingResource")
   421  	s.stub.CheckCall(c, 0, "UploadPendingResource", "mysql", expectedUpload, jsonFile, expectedUploadData)
   422  }
   423  
   424  func (s DeploySuite) TestDeployDockerResourceYAMLFile(c *gc.C) {
   425  	fileContents := `
   426  registrypath: registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image
   427  username: docker-registry
   428  password: hunter2
   429  `
   430  	dir := c.MkDir()
   431  	jsonFile := path.Join(dir, "details.yaml")
   432  	err := ioutil.WriteFile(jsonFile, []byte(fileContents), 0600)
   433  	c.Assert(err, jc.ErrorIsNil)
   434  	deps := uploadDeps{s.stub, rsc{&bytes.Buffer{}}}
   435  	cURL := charm.MustParseURL("cs:~a-user/mysql-k8s-5")
   436  	chID := charmstore.CharmID{
   437  		URL: cURL,
   438  	}
   439  	csMac := &macaroon.Macaroon{}
   440  	resourceMeta := map[string]charmresource.Meta{
   441  		"mysql_image": {
   442  			Name: "mysql_image",
   443  			Type: charmresource.TypeContainerImage,
   444  		},
   445  	}
   446  
   447  	passedResourceValues := map[string]string{
   448  		"mysql_image": jsonFile,
   449  	}
   450  	du := deployUploader{
   451  		applicationID: "mysql",
   452  		chID:          chID,
   453  		csMac:         csMac,
   454  		client:        deps,
   455  		resources:     resourceMeta,
   456  		osOpen:        deps.Open,
   457  		osStat:        deps.Stat,
   458  	}
   459  	ids, err := du.upload(passedResourceValues, map[string]int{})
   460  	c.Assert(err, jc.ErrorIsNil)
   461  	c.Check(ids, gc.DeepEquals, map[string]string{
   462  		"mysql_image": "id-mysql_image",
   463  	})
   464  
   465  	expectedUpload := charmresource.Resource{
   466  		Meta:   resourceMeta["mysql_image"],
   467  		Origin: charmresource.OriginUpload,
   468  	}
   469  
   470  	expectedUploadData := "{\"ImageName\":\"registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image\",\"Username\":\"docker-registry\",\"Password\":\"hunter2\"}"
   471  	s.stub.CheckCallNames(c, "UploadPendingResource")
   472  	s.stub.CheckCall(c, 0, "UploadPendingResource", "mysql", expectedUpload, jsonFile, expectedUploadData)
   473  }
   474  
   475  func (s DeploySuite) TestUnMarshallingDockerDetails(c *gc.C) {
   476  	content := `
   477  registrypath: registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image
   478  username: docker-registry
   479  password: hunter2
   480  `
   481  	data := bytes.NewBufferString(content)
   482  	dets, err := unMarshalDockerDetails(data)
   483  	c.Assert(err, jc.ErrorIsNil)
   484  	c.Assert(dets, gc.DeepEquals, resources.DockerImageDetails{
   485  		RegistryPath: "registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image",
   486  		Username:     "docker-registry",
   487  		Password:     "hunter2",
   488  	})
   489  
   490  	content = `
   491  {
   492  "ImageName": "registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image",
   493  "Username": "docker-registry",
   494  "Password": "hunter2"
   495  }
   496  `
   497  	data = bytes.NewBufferString(content)
   498  	dets, err = unMarshalDockerDetails(data)
   499  	c.Assert(err, jc.ErrorIsNil)
   500  	c.Assert(dets, gc.DeepEquals, resources.DockerImageDetails{
   501  		RegistryPath: "registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image",
   502  		Username:     "docker-registry",
   503  		Password:     "hunter2",
   504  	})
   505  
   506  	content = `
   507  path: registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image@sha256:516f74
   508  username: docker-registry
   509  password: hunter2
   510  `
   511  	data = bytes.NewBufferString(content)
   512  	dets, err = unMarshalDockerDetails(data)
   513  	c.Assert(err, gc.ErrorMatches, "docker image path \"\" not valid")
   514  }
   515  
   516  func (s DeploySuite) TestGetDockerDetailsData(c *gc.C) {
   517  	result, err := getDockerDetailsData("registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image")
   518  	c.Assert(err, jc.ErrorIsNil)
   519  	c.Assert(result, gc.DeepEquals, resources.DockerImageDetails{
   520  		RegistryPath: "registry.staging.jujucharms.com/wallyworld/mysql-k8s/mysql_image",
   521  		Username:     "",
   522  		Password:     "",
   523  	})
   524  
   525  	result, err = getDockerDetailsData("/path/doesnt/exist.yaml")
   526  	c.Assert(err, gc.ErrorMatches, "filepath or registry path: /path/doesnt/exist.yaml not valid")
   527  
   528  	result, err = getDockerDetailsData(".invalid-reg-path")
   529  	c.Assert(err, gc.ErrorMatches, "filepath or registry path: .invalid-reg-path not valid")
   530  
   531  	dir := c.MkDir()
   532  	yamlFile := path.Join(dir, "actually-yaml-file")
   533  	err = ioutil.WriteFile(yamlFile, []byte("registrypath: mariadb/mariadb:10.2"), 0600)
   534  	c.Assert(err, jc.ErrorIsNil)
   535  	result, err = getDockerDetailsData(yamlFile)
   536  	c.Assert(err, jc.ErrorIsNil)
   537  	c.Assert(result, gc.DeepEquals, resources.DockerImageDetails{
   538  		RegistryPath: "mariadb/mariadb:10.2",
   539  		Username:     "",
   540  		Password:     "",
   541  	})
   542  }
   543  
   544  type uploadDeps struct {
   545  	stub           *testing.Stub
   546  	ReadSeekCloser ReadSeekCloser
   547  }
   548  
   549  func (s uploadDeps) AddPendingResources(applicationID string, charmID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (ids []string, err error) {
   550  	charmresource.Sort(resources)
   551  	s.stub.AddCall("AddPendingResources", applicationID, charmID, csMac, resources)
   552  	if err := s.stub.NextErr(); err != nil {
   553  		return nil, err
   554  	}
   555  	ids = make([]string, len(resources))
   556  	for i, res := range resources {
   557  		ids[i] = "id-" + res.Name
   558  	}
   559  	return ids, nil
   560  }
   561  
   562  func (s uploadDeps) UploadPendingResource(applicationID string, resource charmresource.Resource, filename string, r io.ReadSeeker) (id string, err error) {
   563  	data := new(bytes.Buffer)
   564  
   565  	// we care the right data has been passed, not the right io.ReaderSeeker pointer.
   566  	_, err = data.ReadFrom(r)
   567  	if err != nil {
   568  		return "", err
   569  	}
   570  	s.stub.AddCall("UploadPendingResource", applicationID, resource, filename, data.String())
   571  	if err := s.stub.NextErr(); err != nil {
   572  		return "", err
   573  	}
   574  	return "id-" + resource.Name, nil
   575  }
   576  
   577  func (s uploadDeps) Open(name string) (ReadSeekCloser, error) {
   578  	s.stub.AddCall("Open", name)
   579  	if err := s.stub.NextErr(); err != nil {
   580  		return nil, err
   581  	}
   582  	return s.ReadSeekCloser, nil
   583  }
   584  
   585  func (s uploadDeps) Stat(name string) error {
   586  	s.stub.AddCall("Stat", name)
   587  	return s.stub.NextErr()
   588  }
   589  
   590  type rsc struct {
   591  	*bytes.Buffer
   592  }
   593  
   594  func (rsc) Close() error {
   595  	return nil
   596  }
   597  func (rsc) Seek(offset int64, whence int) (int64, error) {
   598  	return 0, nil
   599  }