addon_update.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package gcp
  14. import (
  15. "bytes"
  16. "context"
  17. "fmt"
  18. "io"
  19. "os"
  20. "strings"
  21. "time"
  22. "golang.org/x/crypto/ssh"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/labels"
  25. "k8s.io/apimachinery/pkg/util/wait"
  26. clientset "k8s.io/client-go/kubernetes"
  27. "k8s.io/kubernetes/test/e2e/framework"
  28. e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
  29. e2essh "k8s.io/kubernetes/test/e2e/framework/ssh"
  30. testutils "k8s.io/kubernetes/test/utils"
  31. imageutils "k8s.io/kubernetes/test/utils/image"
  32. "github.com/onsi/ginkgo"
  33. )
  34. // TODO: it would probably be slightly better to build up the objects
  35. // in the code and then serialize to yaml.
  36. var reconcileAddonController = `
  37. apiVersion: v1
  38. kind: ReplicationController
  39. metadata:
  40. name: addon-reconcile-test
  41. namespace: %s
  42. labels:
  43. k8s-app: addon-reconcile-test
  44. kubernetes.io/cluster-service: "true"
  45. addonmanager.kubernetes.io/mode: Reconcile
  46. spec:
  47. replicas: 2
  48. selector:
  49. k8s-app: addon-reconcile-test
  50. template:
  51. metadata:
  52. labels:
  53. k8s-app: addon-reconcile-test
  54. spec:
  55. containers:
  56. - image: %s
  57. name: addon-reconcile-test
  58. ports:
  59. - containerPort: 9376
  60. protocol: TCP
  61. `
  62. // Should update "reconcile" class addon.
  63. var reconcileAddonControllerUpdated = `
  64. apiVersion: v1
  65. kind: ReplicationController
  66. metadata:
  67. name: addon-reconcile-test
  68. namespace: %s
  69. labels:
  70. k8s-app: addon-reconcile-test
  71. kubernetes.io/cluster-service: "true"
  72. addonmanager.kubernetes.io/mode: Reconcile
  73. newLabel: addon-reconcile-test
  74. spec:
  75. replicas: 2
  76. selector:
  77. k8s-app: addon-reconcile-test
  78. template:
  79. metadata:
  80. labels:
  81. k8s-app: addon-reconcile-test
  82. spec:
  83. containers:
  84. - image: %s
  85. name: addon-reconcile-test
  86. ports:
  87. - containerPort: 9376
  88. protocol: TCP
  89. `
  90. var ensureExistsAddonService = `
  91. apiVersion: v1
  92. kind: Service
  93. metadata:
  94. name: addon-ensure-exists-test
  95. namespace: %s
  96. labels:
  97. k8s-app: addon-ensure-exists-test
  98. addonmanager.kubernetes.io/mode: EnsureExists
  99. spec:
  100. ports:
  101. - port: 9376
  102. protocol: TCP
  103. targetPort: 9376
  104. selector:
  105. k8s-app: addon-ensure-exists-test
  106. `
  107. // Should create but don't update "ensure exist" class addon.
  108. var ensureExistsAddonServiceUpdated = `
  109. apiVersion: v1
  110. kind: Service
  111. metadata:
  112. name: addon-ensure-exists-test
  113. namespace: %s
  114. labels:
  115. k8s-app: addon-ensure-exists-test
  116. addonmanager.kubernetes.io/mode: EnsureExists
  117. newLabel: addon-ensure-exists-test
  118. spec:
  119. ports:
  120. - port: 9376
  121. protocol: TCP
  122. targetPort: 9376
  123. selector:
  124. k8s-app: addon-ensure-exists-test
  125. `
  126. var deprecatedLabelAddonService = `
  127. apiVersion: v1
  128. kind: Service
  129. metadata:
  130. name: addon-deprecated-label-test
  131. namespace: %s
  132. labels:
  133. k8s-app: addon-deprecated-label-test
  134. kubernetes.io/cluster-service: "true"
  135. spec:
  136. ports:
  137. - port: 9376
  138. protocol: TCP
  139. targetPort: 9376
  140. selector:
  141. k8s-app: addon-deprecated-label-test
  142. `
  143. // Should update addon with label "kubernetes.io/cluster-service=true".
  144. var deprecatedLabelAddonServiceUpdated = `
  145. apiVersion: v1
  146. kind: Service
  147. metadata:
  148. name: addon-deprecated-label-test
  149. namespace: %s
  150. labels:
  151. k8s-app: addon-deprecated-label-test
  152. kubernetes.io/cluster-service: "true"
  153. newLabel: addon-deprecated-label-test
  154. spec:
  155. ports:
  156. - port: 9376
  157. protocol: TCP
  158. targetPort: 9376
  159. selector:
  160. k8s-app: addon-deprecated-label-test
  161. `
  162. // Should not create addon without valid label.
  163. var invalidAddonController = `
  164. apiVersion: v1
  165. kind: ReplicationController
  166. metadata:
  167. name: invalid-addon-test
  168. namespace: %s
  169. labels:
  170. k8s-app: invalid-addon-test
  171. addonmanager.kubernetes.io/mode: NotMatch
  172. spec:
  173. replicas: 2
  174. selector:
  175. k8s-app: invalid-addon-test
  176. template:
  177. metadata:
  178. labels:
  179. k8s-app: invalid-addon-test
  180. spec:
  181. containers:
  182. - image: %s
  183. name: invalid-addon-test
  184. ports:
  185. - containerPort: 9376
  186. protocol: TCP
  187. `
  188. const (
  189. addonTestPollInterval = 3 * time.Second
  190. addonTestPollTimeout = 5 * time.Minute
  191. addonNsName = metav1.NamespaceSystem
  192. )
  193. var serveHostnameImage = imageutils.GetE2EImage(imageutils.Agnhost)
  194. type stringPair struct {
  195. data, fileName string
  196. }
  197. var _ = SIGDescribe("Addon update", func() {
  198. var dir string
  199. var sshClient *ssh.Client
  200. f := framework.NewDefaultFramework("addon-update-test")
  201. ginkgo.BeforeEach(func() {
  202. // This test requires:
  203. // - SSH master access
  204. // ... so the provider check should be identical to the intersection of
  205. // providers that provide those capabilities.
  206. if !framework.ProviderIs("gce") {
  207. return
  208. }
  209. var err error
  210. sshClient, err = getMasterSSHClient()
  211. framework.ExpectNoError(err, "Failed to get the master SSH client.")
  212. })
  213. ginkgo.AfterEach(func() {
  214. if sshClient != nil {
  215. sshClient.Close()
  216. }
  217. })
  218. // WARNING: the test is not parallel-friendly!
  219. ginkgo.It("should propagate add-on file changes [Slow]", func() {
  220. // This test requires:
  221. // - SSH
  222. // - master access
  223. // ... so the provider check should be identical to the intersection of
  224. // providers that provide those capabilities.
  225. e2eskipper.SkipUnlessProviderIs("gce")
  226. //these tests are long, so I squeezed several cases in one scenario
  227. framework.ExpectNotEqual(sshClient, nil)
  228. dir = f.Namespace.Name // we use it only to give a unique string for each test execution
  229. temporaryRemotePathPrefix := "addon-test-dir"
  230. temporaryRemotePath := temporaryRemotePathPrefix + "/" + dir // in home directory on kubernetes-master
  231. defer sshExec(sshClient, fmt.Sprintf("rm -rf %s", temporaryRemotePathPrefix)) // ignore the result in cleanup
  232. sshExecAndVerify(sshClient, fmt.Sprintf("mkdir -p %s", temporaryRemotePath))
  233. rcAddonReconcile := "addon-reconcile-controller.yaml"
  234. rcAddonReconcileUpdated := "addon-reconcile-controller-Updated.yaml"
  235. rcInvalid := "invalid-addon-controller.yaml"
  236. svcAddonDeprecatedLabel := "addon-deprecated-label-service.yaml"
  237. svcAddonDeprecatedLabelUpdated := "addon-deprecated-label-service-updated.yaml"
  238. svcAddonEnsureExists := "addon-ensure-exists-service.yaml"
  239. svcAddonEnsureExistsUpdated := "addon-ensure-exists-service-updated.yaml"
  240. var remoteFiles []stringPair = []stringPair{
  241. {fmt.Sprintf(reconcileAddonController, addonNsName, serveHostnameImage), rcAddonReconcile},
  242. {fmt.Sprintf(reconcileAddonControllerUpdated, addonNsName, serveHostnameImage), rcAddonReconcileUpdated},
  243. {fmt.Sprintf(deprecatedLabelAddonService, addonNsName), svcAddonDeprecatedLabel},
  244. {fmt.Sprintf(deprecatedLabelAddonServiceUpdated, addonNsName), svcAddonDeprecatedLabelUpdated},
  245. {fmt.Sprintf(ensureExistsAddonService, addonNsName), svcAddonEnsureExists},
  246. {fmt.Sprintf(ensureExistsAddonServiceUpdated, addonNsName), svcAddonEnsureExistsUpdated},
  247. {fmt.Sprintf(invalidAddonController, addonNsName, serveHostnameImage), rcInvalid},
  248. }
  249. for _, p := range remoteFiles {
  250. err := writeRemoteFile(sshClient, p.data, temporaryRemotePath, p.fileName, 0644)
  251. framework.ExpectNoError(err, "Failed to write file %q at remote path %q with ssh client %+v", p.fileName, temporaryRemotePath, sshClient)
  252. }
  253. // directory on kubernetes-master
  254. destinationDirPrefix := "/etc/kubernetes/addons/addon-test-dir"
  255. destinationDir := destinationDirPrefix + "/" + dir
  256. // cleanup from previous tests
  257. _, _, _, err := sshExec(sshClient, fmt.Sprintf("sudo rm -rf %s", destinationDirPrefix))
  258. framework.ExpectNoError(err, "Failed to remove remote dir %q with ssh client %+v", destinationDirPrefix, sshClient)
  259. defer sshExec(sshClient, fmt.Sprintf("sudo rm -rf %s", destinationDirPrefix)) // ignore result in cleanup
  260. sshExecAndVerify(sshClient, fmt.Sprintf("sudo mkdir -p %s", destinationDir))
  261. ginkgo.By("copy invalid manifests to the destination dir")
  262. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcInvalid, destinationDir, rcInvalid))
  263. // we will verify at the end of the test that the objects weren't created from the invalid manifests
  264. ginkgo.By("copy new manifests")
  265. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcAddonReconcile, destinationDir, rcAddonReconcile))
  266. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonDeprecatedLabel, destinationDir, svcAddonDeprecatedLabel))
  267. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonEnsureExists, destinationDir, svcAddonEnsureExists))
  268. // Delete the "ensure exist class" addon at the end.
  269. defer func() {
  270. framework.Logf("Cleaning up ensure exist class addon.")
  271. err := f.ClientSet.CoreV1().Services(addonNsName).Delete(context.TODO(), "addon-ensure-exists-test", nil)
  272. framework.ExpectNoError(err)
  273. }()
  274. waitForReplicationControllerInAddonTest(f.ClientSet, addonNsName, "addon-reconcile-test", true)
  275. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-deprecated-label-test", true)
  276. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-ensure-exists-test", true)
  277. // Replace the manifests with new contents.
  278. ginkgo.By("update manifests")
  279. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcAddonReconcileUpdated, destinationDir, rcAddonReconcile))
  280. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonDeprecatedLabelUpdated, destinationDir, svcAddonDeprecatedLabel))
  281. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonEnsureExistsUpdated, destinationDir, svcAddonEnsureExists))
  282. // Wait for updated addons to have the new added label.
  283. reconcileSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-reconcile-test"}))
  284. waitForReplicationControllerwithSelectorInAddonTest(f.ClientSet, addonNsName, true, reconcileSelector)
  285. deprecatedLabelSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-deprecated-label-test"}))
  286. waitForServicewithSelectorInAddonTest(f.ClientSet, addonNsName, true, deprecatedLabelSelector)
  287. // "Ensure exist class" addon should not be updated.
  288. ensureExistSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-ensure-exists-test"}))
  289. waitForServicewithSelectorInAddonTest(f.ClientSet, addonNsName, false, ensureExistSelector)
  290. ginkgo.By("remove manifests")
  291. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, rcAddonReconcile))
  292. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, svcAddonDeprecatedLabel))
  293. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, svcAddonEnsureExists))
  294. waitForReplicationControllerInAddonTest(f.ClientSet, addonNsName, "addon-reconcile-test", false)
  295. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-deprecated-label-test", false)
  296. // "Ensure exist class" addon will not be deleted when manifest is removed.
  297. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-ensure-exists-test", true)
  298. ginkgo.By("verify invalid addons weren't created")
  299. _, err = f.ClientSet.CoreV1().ReplicationControllers(addonNsName).Get(context.TODO(), "invalid-addon-test", metav1.GetOptions{})
  300. framework.ExpectError(err)
  301. // Invalid addon manifests and the "ensure exist class" addon will be deleted by the deferred function.
  302. })
  303. })
  304. func waitForServiceInAddonTest(c clientset.Interface, addonNamespace, name string, exist bool) {
  305. framework.ExpectNoError(framework.WaitForService(c, addonNamespace, name, exist, addonTestPollInterval, addonTestPollTimeout))
  306. }
  307. func waitForReplicationControllerInAddonTest(c clientset.Interface, addonNamespace, name string, exist bool) {
  308. framework.ExpectNoError(waitForReplicationController(c, addonNamespace, name, exist, addonTestPollInterval, addonTestPollTimeout))
  309. }
  310. func waitForServicewithSelectorInAddonTest(c clientset.Interface, addonNamespace string, exist bool, selector labels.Selector) {
  311. framework.ExpectNoError(waitForServiceWithSelector(c, addonNamespace, selector, exist, addonTestPollInterval, addonTestPollTimeout))
  312. }
  313. func waitForReplicationControllerwithSelectorInAddonTest(c clientset.Interface, addonNamespace string, exist bool, selector labels.Selector) {
  314. framework.ExpectNoError(waitForReplicationControllerWithSelector(c, addonNamespace, selector, exist, addonTestPollInterval,
  315. addonTestPollTimeout))
  316. }
  317. // waitForReplicationController waits until the RC appears (exist == true), or disappears (exist == false)
  318. func waitForReplicationController(c clientset.Interface, namespace, name string, exist bool, interval, timeout time.Duration) error {
  319. err := wait.PollImmediate(interval, timeout, func() (bool, error) {
  320. _, err := c.CoreV1().ReplicationControllers(namespace).Get(context.TODO(), name, metav1.GetOptions{})
  321. if err != nil {
  322. framework.Logf("Get ReplicationController %s in namespace %s failed (%v).", name, namespace, err)
  323. return !exist, nil
  324. }
  325. framework.Logf("ReplicationController %s in namespace %s found.", name, namespace)
  326. return exist, nil
  327. })
  328. if err != nil {
  329. stateMsg := map[bool]string{true: "to appear", false: "to disappear"}
  330. return fmt.Errorf("error waiting for ReplicationController %s/%s %s: %v", namespace, name, stateMsg[exist], err)
  331. }
  332. return nil
  333. }
  334. // waitForServiceWithSelector waits until any service with given selector appears (exist == true), or disappears (exist == false)
  335. func waitForServiceWithSelector(c clientset.Interface, namespace string, selector labels.Selector, exist bool, interval,
  336. timeout time.Duration) error {
  337. err := wait.PollImmediate(interval, timeout, func() (bool, error) {
  338. services, err := c.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
  339. switch {
  340. case len(services.Items) != 0:
  341. framework.Logf("Service with %s in namespace %s found.", selector.String(), namespace)
  342. return exist, nil
  343. case len(services.Items) == 0:
  344. framework.Logf("Service with %s in namespace %s disappeared.", selector.String(), namespace)
  345. return !exist, nil
  346. case !testutils.IsRetryableAPIError(err):
  347. framework.Logf("Non-retryable failure while listing service.")
  348. return false, err
  349. default:
  350. framework.Logf("List service with %s in namespace %s failed: %v", selector.String(), namespace, err)
  351. return false, nil
  352. }
  353. })
  354. if err != nil {
  355. stateMsg := map[bool]string{true: "to appear", false: "to disappear"}
  356. return fmt.Errorf("error waiting for service with %s in namespace %s %s: %v", selector.String(), namespace, stateMsg[exist], err)
  357. }
  358. return nil
  359. }
  360. // waitForReplicationControllerWithSelector waits until any RC with given selector appears (exist == true), or disappears (exist == false)
  361. func waitForReplicationControllerWithSelector(c clientset.Interface, namespace string, selector labels.Selector, exist bool, interval,
  362. timeout time.Duration) error {
  363. err := wait.PollImmediate(interval, timeout, func() (bool, error) {
  364. rcs, err := c.CoreV1().ReplicationControllers(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
  365. switch {
  366. case len(rcs.Items) != 0:
  367. framework.Logf("ReplicationController with %s in namespace %s found.", selector.String(), namespace)
  368. return exist, nil
  369. case len(rcs.Items) == 0:
  370. framework.Logf("ReplicationController with %s in namespace %s disappeared.", selector.String(), namespace)
  371. return !exist, nil
  372. default:
  373. framework.Logf("List ReplicationController with %s in namespace %s failed: %v", selector.String(), namespace, err)
  374. return false, nil
  375. }
  376. })
  377. if err != nil {
  378. stateMsg := map[bool]string{true: "to appear", false: "to disappear"}
  379. return fmt.Errorf("error waiting for ReplicationControllers with %s in namespace %s %s: %v", selector.String(), namespace, stateMsg[exist], err)
  380. }
  381. return nil
  382. }
  383. // TODO use the ssh.SSH code, either adding an SCP to it or copying files
  384. // differently.
  385. func getMasterSSHClient() (*ssh.Client, error) {
  386. // Get a signer for the provider.
  387. signer, err := e2essh.GetSigner(framework.TestContext.Provider)
  388. if err != nil {
  389. return nil, fmt.Errorf("error getting signer for provider %s: '%v'", framework.TestContext.Provider, err)
  390. }
  391. sshUser := os.Getenv("KUBE_SSH_USER")
  392. if sshUser == "" {
  393. sshUser = os.Getenv("USER")
  394. }
  395. config := &ssh.ClientConfig{
  396. User: sshUser,
  397. Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
  398. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  399. }
  400. host := framework.GetMasterHost() + ":22"
  401. client, err := ssh.Dial("tcp", host, config)
  402. if err != nil {
  403. return nil, fmt.Errorf("error getting SSH client to host %s: '%v'", host, err)
  404. }
  405. return client, err
  406. }
  407. func sshExecAndVerify(client *ssh.Client, cmd string) {
  408. _, _, rc, err := sshExec(client, cmd)
  409. framework.ExpectNoError(err, "Failed to execute %q with ssh client %+v", cmd, client)
  410. framework.ExpectEqual(rc, 0, "error return code from executing command on the cluster: %s", cmd)
  411. }
  412. func sshExec(client *ssh.Client, cmd string) (string, string, int, error) {
  413. framework.Logf("Executing '%s' on %v", cmd, client.RemoteAddr())
  414. session, err := client.NewSession()
  415. if err != nil {
  416. return "", "", 0, fmt.Errorf("error creating session to host %s: '%v'", client.RemoteAddr(), err)
  417. }
  418. defer session.Close()
  419. // Run the command.
  420. code := 0
  421. var bout, berr bytes.Buffer
  422. session.Stdout, session.Stderr = &bout, &berr
  423. err = session.Run(cmd)
  424. if err != nil {
  425. // Check whether the command failed to run or didn't complete.
  426. if exiterr, ok := err.(*ssh.ExitError); ok {
  427. // If we got an ExitError and the exit code is nonzero, we'll
  428. // consider the SSH itself successful (just that the command run
  429. // errored on the host).
  430. if code = exiterr.ExitStatus(); code != 0 {
  431. err = nil
  432. }
  433. } else {
  434. // Some other kind of error happened (e.g. an IOError); consider the
  435. // SSH unsuccessful.
  436. err = fmt.Errorf("failed running `%s` on %s: '%v'", cmd, client.RemoteAddr(), err)
  437. }
  438. }
  439. return bout.String(), berr.String(), code, err
  440. }
  441. func writeRemoteFile(sshClient *ssh.Client, data, dir, fileName string, mode os.FileMode) error {
  442. framework.Logf(fmt.Sprintf("Writing remote file '%s/%s' on %v", dir, fileName, sshClient.RemoteAddr()))
  443. session, err := sshClient.NewSession()
  444. if err != nil {
  445. return fmt.Errorf("error creating session to host %s: '%v'", sshClient.RemoteAddr(), err)
  446. }
  447. defer session.Close()
  448. fileSize := len(data)
  449. pipe, err := session.StdinPipe()
  450. if err != nil {
  451. return err
  452. }
  453. defer pipe.Close()
  454. if err := session.Start(fmt.Sprintf("scp -t %s", dir)); err != nil {
  455. return err
  456. }
  457. fmt.Fprintf(pipe, "C%#o %d %s\n", mode, fileSize, fileName)
  458. io.Copy(pipe, strings.NewReader(data))
  459. fmt.Fprint(pipe, "\x00")
  460. pipe.Close()
  461. return session.Wait()
  462. }