func makeResultSet(t *testing.T) resultset {
	temp, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		t.Fatal(err)
	}

	context, _, err := runtime.NewTaskContext(temp.NewFilePath(), runtime.TaskInfo{}, nil)
	if err != nil {
		t.Fatal(err)
	}

	config := configType{
		CreateUser: false,
	}

	e := engine{
		config: &config,
		log:    logrus.New().WithField("component", "test"),
	}

	return resultset{
		ResultSetBase: engines.ResultSetBase{},
		taskUser:      user{},
		context:       context,
		success:       true,
		engine:        &e,
	}
}
// NewTestEnvironment creates a new Environment suitable for use in tests.
//
// This function should only be used in testing
func newTestEnvironment() *runtime.Environment {
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	nilOrPanic(err, "Failed to create temporary storage at: ", os.TempDir())

	folder, err := storage.NewFolder()
	nilOrPanic(err, "Failed to create temporary storage folder")

	// Set finalizer so that we always get the temporary folder removed.
	// This is should really only be used in tests, otherwise it would better to
	// call Remove() manually.
	rt.SetFinalizer(folder, func(f runtime.TemporaryFolder) {
		f.Remove()
	})

	logger, err := runtime.CreateLogger(os.Getenv("LOGGING_LEVEL"))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger. %s", err)
		os.Exit(1)
	}

	return &runtime.Environment{
		GarbageCollector: &gc.GarbageCollector{},
		TemporaryStorage: folder,
		Log:              logger,
	}
}
func ensureEnvironment(t *testing.T) (*runtime.Environment, engines.Engine, plugins.Plugin) {
	tempPath := filepath.Join(os.TempDir(), slugid.Nice())
	tempStorage, err := runtime.NewTemporaryStorage(tempPath)
	if err != nil {
		t.Fatal(err)
	}

	environment := &runtime.Environment{
		TemporaryStorage: tempStorage,
	}
	engineProvider := engines.Engines()["mock"]
	engine, err := engineProvider.NewEngine(engines.EngineOptions{
		Environment: environment,
		Log:         logger.WithField("engine", "mock"),
	})
	if err != nil {
		t.Fatal(err.Error())
	}

	pluginOptions := plugins.PluginOptions{
		Environment: environment,
		Engine:      engine,
		Log:         logger.WithField("component", "Plugin Manager"),
	}

	pm, err := plugins.Plugins()["success"].NewPlugin(pluginOptions)
	if err != nil {
		t.Fatalf("Error creating task manager. Could not create plugin manager. %s", err)
	}

	return environment, engine, pm
}
func TestGuestToolsFailed(t *testing.T) {
	// Create temporary storage
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}
	environment := &runtime.Environment{
		TemporaryStorage: storage,
	}

	// Setup a new MetaService
	logTask := bytes.NewBuffer(nil)
	result := false
	resolved := false
	m := sync.Mutex{}
	s := metaservice.New([]string{"sh", "-c", "echo \"$TEST_TEXT\" && false"}, map[string]string{
		"TEST_TEXT": "Hello world",
	}, logTask, func(r bool) {
		m.Lock()
		defer m.Unlock()
		if resolved {
			panic("It shouldn't be possible to resolve twice")
		}
		resolved = true
		result = r
	}, environment)

	// Create http server for testing
	ts := httptest.NewServer(s)
	defer ts.Close()
	u, err := url.Parse(ts.URL)
	if err != nil {
		panic("Expected a url we can parse")
	}

	// Create a logger
	logger, _ := runtime.CreateLogger("info")
	log := logger.WithField("component", "guest-tools-tests")

	// Create an run guest-tools
	g := new(u.Host, log)
	g.Run()

	// Check the state
	if !resolved {
		t.Error("Expected the metadata to have resolved the task")
	}
	if result != false {
		t.Error("Expected the metadata to get failed result")
	}
	if !strings.Contains(logTask.String(), "Hello world") {
		t.Error("Got unexpected taskLog: '", logTask.String(), "'")
	}
}
func newTestSandbox(taskPayload *payloadType, env []string) (*sandbox, error) {
	temp, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		return nil, err
	}

	context, _, err := runtime.NewTaskContext(temp.NewFilePath(), runtime.TaskInfo{}, nil)
	if err != nil {
		return nil, err
	}

	config := configType{
		CreateUser: false,
	}

	e := engine{
		EngineBase: engines.EngineBase{},
		config:     &config,
		log:        logrus.New().WithField("component", "test"),
	}

	return newSandbox(context, taskPayload, env, &e), nil
}
// New will create a worker given configuration matching the schema from
// ConfigSchema(). The log parameter is optional and if nil is given a default
// logrus logger will be used.
func New(config interface{}, log *logrus.Logger) (*Worker, error) {
	// Validate and map configuration to c
	var c configType
	if err := schematypes.MustMap(ConfigSchema(), config, &c); err != nil {
		return nil, fmt.Errorf("Invalid configuration: %s", err)
	}

	// Create temporary folder
	err := os.RemoveAll(c.TemporaryFolder)
	if err != nil {
		return nil, fmt.Errorf("Failed to remove temporaryFolder: %s, error: %s",
			c.TemporaryFolder, err)
	}
	tempStorage, err := runtime.NewTemporaryStorage(c.TemporaryFolder)
	if err != nil {
		return nil, fmt.Errorf("Failed to create temporary folder, error: %s", err)
	}

	// Create logger
	if log == nil {
		log = logrus.New()
	}
	log.Level, _ = logrus.ParseLevel(c.LogLevel)

	// Setup WebHookServer
	localServer, err := webhookserver.NewLocalServer(
		net.ParseIP(c.ServerIP), c.ServerPort,
		c.NetworkInterface, c.ExposedPort,
		c.DNSDomain,
		c.DNSSecret,
		c.TLSCertificate,
		c.TLSKey,
		time.Duration(c.MaxLifeCycle)*time.Second,
	)
	if err != nil {
		return nil, err
	}

	// Create environment
	gc := gc.New(c.TemporaryFolder, c.MinimumDiskSpace, c.MinimumMemory)
	env := &runtime.Environment{
		GarbageCollector: gc,
		Log:              log,
		TemporaryStorage: tempStorage,
		WebHookServer:    localServer,
	}

	// Ensure that engine confiuguration was provided for the engine selected
	if _, ok := c.Engines[c.Engine]; !ok {
		return nil, fmt.Errorf("Invalid configuration: The key 'engines.%s' must "+
			"be specified when engine '%s' is selected", c.Engine, c.Engine)
	}

	// Find engine provider (schema should ensure it exists)
	provider := engines.Engines()[c.Engine]
	engine, err := provider.NewEngine(engines.EngineOptions{
		Environment: env,
		Log:         env.Log.WithField("engine", c.Engine),
		Config:      c.Engines[c.Engine],
	})
	if err != nil {
		return nil, fmt.Errorf("Engine initialization failed, error: %s", err)
	}

	// Initialize plugin manager
	pm, err := plugins.NewPluginManager(plugins.PluginOptions{
		Environment: env,
		Engine:      engine,
		Log:         env.Log.WithField("plugin", "plugin-manager"),
		Config:      c.Plugins,
	})
	if err != nil {
		return nil, fmt.Errorf("Plugin initialization failed, error: %s", err)
	}

	tm, err := newTaskManager(
		&c, engine, pm, env,
		env.Log.WithField("component", "task-manager"), gc,
	)
	if err != nil {
		return nil, err
	}

	return &Worker{
		log:    env.Log.WithField("component", "worker"),
		tm:     tm,
		sm:     runtime.NewShutdownManager("local"),
		env:    env,
		server: localServer,
		done:   make(chan struct{}),
	}, nil
}
func TestGuestToolsProcessingActions(t *testing.T) {
	// Create temporary storage
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}
	environment := &runtime.Environment{
		TemporaryStorage: storage,
	}

	logTask := bytes.NewBuffer(nil)
	meta := metaservice.New([]string{}, map[string]string{}, logTask, func(r bool) {
		panic("This test shouldn't get to this point!")
	}, environment)

	// Create http server for testing
	ts := httptest.NewServer(meta)
	defer ts.Close()
	defer meta.StopPollers() // Hack to stop pollers, otherwise server will block
	u, err := url.Parse(ts.URL)
	if err != nil {
		panic("Expected a url we can parse")
	}

	// Create a logger
	logger, _ := runtime.CreateLogger("info")
	log := logger.WithField("component", "guest-tools-tests")

	// Create an run guest-tools
	g := new(u.Host, log)

	// start processing actions
	go g.ProcessActions()
	defer g.StopProcessingActions()

	////////////////////
	debug("### Test meta.GetArtifact")
	f, err := storage.NewFolder()
	if err != nil {
		panic("Failed to create temp folder")
	}
	defer f.Remove()

	testFile := filepath.Join(f.Path(), "hello.txt")
	err = ioutil.WriteFile(testFile, []byte("hello-world"), 0777)
	nilOrPanic(err, "Failed to create testFile: ", testFile)

	debug(" - request file: %s", testFile)
	r, err := meta.GetArtifact(testFile)
	nilOrPanic(err, "meta.GetArtifact failed, error: ", err)

	debug(" - reading testFile")
	data, err := ioutil.ReadAll(r)
	nilOrPanic(err, "Failed to read testFile")
	debug(" - read: '%s'", string(data))
	assert(string(data) == "hello-world", "Wrong payload: ", string(data))

	////////////////////
	debug("### Test meta.GetArtifact (missing file)")
	r, err = meta.GetArtifact(filepath.Join(f.Path(), "missing-file.txt"))
	assert(r == nil, "Expected error wihtout a reader")
	assert(err == engines.ErrResourceNotFound, "Expected ErrResourceNotFound")

	////////////////////
	debug("### Test meta.ListFolder")
	testFolder := filepath.Join(f.Path(), "test-folder")
	err = os.Mkdir(testFolder, 0777)
	nilOrPanic(err, "Failed to create test-folder/")

	testFile2 := filepath.Join(testFolder, "hello2.txt")
	err = ioutil.WriteFile(testFile2, []byte("hello-world-2"), 0777)
	nilOrPanic(err, "Failed to create testFile2: ", testFile2)

	debug(" - meta.ListFolder")
	files, err := meta.ListFolder(f.Path())
	nilOrPanic(err, "ListFolder failed, err: ", err)

	assert(len(files) == 2, "Expected 2 files")
	assert(files[0] == testFile || files[1] == testFile, "Expected testFile")
	assert(files[0] == testFile2 || files[1] == testFile2, "Expected testFile2")

	////////////////////
	debug("### Test meta.ListFolder (missing folder)")
	files, err = meta.ListFolder(filepath.Join(f.Path(), "no-such-folder"))
	assert(files == nil, "Expected files == nil, we hopefully have an error")
	assert(err == engines.ErrResourceNotFound, "Expected ErrResourceNotFound")

	////////////////////
	debug("### Test meta.ListFolder (empty folder)")
	emptyFolder := filepath.Join(f.Path(), "empty-folder")
	err = os.Mkdir(emptyFolder, 0777)
	nilOrPanic(err, "Failed to create empty-folder/")

	files, err = meta.ListFolder(emptyFolder)
	assert(len(files) == 0, "Expected zero files")
	assert(err == nil, "Didn't expect any error")

	////////////////////
	testShellHello(meta)
	testShellCat(meta)
	testShellCatStdErr(meta)
}
Beispiel #8
0
func (cmd) Execute(arguments map[string]interface{}) bool {
	// Read arguments
	imageFile := arguments["<image>"].(string)
	command := arguments["<command>"].([]string)
	vnc := arguments["--vnc"].(bool)

	// Create temporary storage and environment
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}
	environment := &runtime.Environment{
		TemporaryStorage: storage,
	}

	// Create a temporary folder
	tempFolder := filepath.Join("/tmp", slugid.Nice())
	if err = os.Mkdir(tempFolder, 0777); err != nil {
		log.Fatal("Failed to create temporary folder in /tmp, error: ", err)
	}

	// Create the necessary runtime setup
	gc := &gc.GarbageCollector{}
	logger, _ := runtime.CreateLogger("info")
	log := logger.WithField("component", "qemu-run")

	// Create image manager
	log.Info("Creating image manager")
	manager, err := image.NewManager(filepath.Join(tempFolder, "/images/"), gc, logger.WithField("component", "image-manager"), nil)
	if err != nil {
		log.Fatal("Failed to create image manager", err)
	}

	// Get an instance of the image
	log.Info("Creating instance of image")
	image, err := manager.Instance("image", func(target string) error {
		return cp.CopyFile(target, imageFile)
	})
	if err != nil {
		log.Fatal("Failed to create instance of image, error: ", err)
	}

	// Setup a user-space network
	log.Info("Creating user-space network")
	net, err := network.NewUserNetwork(tempFolder)
	if err != nil {
		log.Fatal("Failed to create user-space network, error: ", err)
	}

	// Create virtual machine
	log.Info("Creating virtual machine")
	vm, err := vm.NewVirtualMachine(
		image.Machine().Options(), image, net, tempFolder,
		"", "", logger.WithField("component", "vm"),
	)
	if err != nil {
		log.Fatal("Failed to create virtual-machine, error: ", err)
	}

	// Create meta-data service
	log.Info("Creating meta-data service")
	var shellServer *interactive.ShellServer
	var displayServer *interactive.DisplayServer
	ms := metaservice.New(command, make(map[string]string), os.Stdout, func(result bool) {
		fmt.Println("### Task Completed, result = ", result)
		shellServer.WaitAndClose()
		displayServer.Abort()
		vm.Kill()
	}, environment)

	// Setup http handler for network
	vm.SetHTTPHandler(ms)

	// Create ShellServer
	shellServer = interactive.NewShellServer(
		ms.ExecShell, log.WithField("component", "shell-server"),
	)

	// Create displayServer
	displayServer = interactive.NewDisplayServer(
		&socketDisplayProvider{socket: vm.VNCSocket()},
		log.WithField("component", "display-server"),
	)

	interactiveHandler := http.NewServeMux()
	interactiveHandler.Handle("/shell/", shellServer)
	interactiveHandler.Handle("/display/", displayServer)
	interactiveServer := graceful.Server{
		Timeout: 30 * time.Second,
		Server: &http.Server{
			Addr:    "localhost:8080",
			Handler: interactiveHandler,
		},
		NoSignalHandling: true,
	}
	go interactiveServer.ListenAndServe()

	// Start the virtual machine
	log.Info("Start the virtual machine")
	vm.Start()

	// Start vncviewer
	done := make(chan struct{})
	if vnc {
		go StartVNCViewer(vm.VNCSocket(), done)
	}

	// Wait for SIGINT/SIGKILL or vm.Done
	c := make(chan os.Signal, 2)
	signal.Notify(c, os.Interrupt, os.Kill) // This pattern leaks, acceptable here
	select {
	case <-c:
		signal.Stop(c)
		fmt.Println("### Terminating QEMU")
		vm.Kill()
	case <-vm.Done:
		fmt.Println("### QEMU terminated")
	}
	close(done)

	// Ensure that QEMU has terminated before we continue
	<-vm.Done
	interactiveServer.Stop(100 * time.Millisecond)

	// Clean up anything left in the garbage collector
	gc.CollectAll()
	return true
}
func TestTaskManagerRunTask(t *testing.T) {
	resolved := false
	var serverURL string
	var handler = func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/artifacts/public/logs/live_backing.log") {
			json.NewEncoder(w).Encode(&queue.S3ArtifactResponse{
				PutURL: serverURL,
			})
			return
		}

		if strings.Contains(r.URL.Path, "/artifacts/public/logs/live.log") {
			json.NewEncoder(w).Encode(&queue.RedirectArtifactResponse{})
			return
		}

		if strings.Contains(r.URL.Path, "/task/abc/runs/1/completed") {
			resolved = true
			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			json.NewEncoder(w).Encode(&queue.TaskStatusResponse{})
		}
	}

	s := httptest.NewServer(http.HandlerFunc(handler))
	serverURL = s.URL
	defer s.Close()

	tempPath := filepath.Join(os.TempDir(), slugid.Nice())
	tempStorage, err := runtime.NewTemporaryStorage(tempPath)
	if err != nil {
		t.Fatal(err)
	}

	localServer, err := webhookserver.NewLocalServer(
		[]byte{127, 0, 0, 1}, 60000,
		"", 0,
		"example.com",
		"",
		"",
		"",
		10*time.Minute,
	)
	if err != nil {
		t.Error(err)
	}

	gc := &gc.GarbageCollector{}
	environment := &runtime.Environment{
		GarbageCollector: gc,
		TemporaryStorage: tempStorage,
		WebHookServer:    localServer,
	}
	engineProvider := engines.Engines()["mock"]
	engine, err := engineProvider.NewEngine(engines.EngineOptions{
		Environment: environment,
		Log:         logger.WithField("engine", "mock"),
	})
	if err != nil {
		t.Fatal(err.Error())
	}

	cfg := &configType{
		QueueBaseURL: serverURL,
	}

	tm, err := newTaskManager(cfg, engine, MockPlugin{}, environment, logger.WithField("test", "TestTaskManagerRunTask"), gc)
	if err != nil {
		t.Fatal(err)
	}

	claim := &taskClaim{
		taskID: "abc",
		runID:  1,
		taskClaim: &queue.TaskClaimResponse{
			Credentials: struct {
				AccessToken string `json:"accessToken"`
				Certificate string `json:"certificate"`
				ClientID    string `json:"clientId"`
			}{
				AccessToken: "123",
				ClientID:    "abc",
				Certificate: "",
			},
			TakenUntil: tcclient.Time(time.Now().Add(time.Minute * 5)),
		},
		definition: &queue.TaskDefinitionResponse{
			Payload: []byte(`{"delay": 1,"function": "write-log","argument": "Hello World"}`),
		},
	}
	tm.run(claim)
	assert.True(t, resolved, "Task was not resolved")
}
func TestWorkerShutdown(t *testing.T) {
	var resCount int32
	var serverURL string

	var handler = func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/artifacts/public/logs/live_backing.log") {
			json.NewEncoder(w).Encode(&queue.S3ArtifactResponse{
				PutURL: serverURL,
			})
			return
		}

		if strings.Contains(r.URL.Path, "/artifacts/public/logs/live.log") {
			json.NewEncoder(w).Encode(&queue.RedirectArtifactResponse{})
			return
		}

		if strings.Contains(r.URL.Path, "exception") {
			var exception queue.TaskExceptionRequest
			err := json.NewDecoder(r.Body).Decode(&exception)
			// Ignore errors for now
			if err != nil {
				return
			}

			assert.Equal(t, "worker-shutdown", exception.Reason)
			atomic.AddInt32(&resCount, 1)

			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			json.NewEncoder(w).Encode(&queue.TaskStatusResponse{})
		}
	}

	s := httptest.NewServer(http.HandlerFunc(handler))
	serverURL = s.URL
	defer s.Close()

	tempPath := filepath.Join(os.TempDir(), slugid.Nice())
	tempStorage, err := runtime.NewTemporaryStorage(tempPath)
	if err != nil {
		t.Fatal(err)
	}

	localServer, err := webhookserver.NewLocalServer(
		[]byte{127, 0, 0, 1}, 60000,
		"", 0,
		"example.com",
		"",
		"",
		"",
		10*time.Minute,
	)
	if err != nil {
		t.Error(err)
	}

	gc := &gc.GarbageCollector{}
	environment := &runtime.Environment{
		GarbageCollector: gc,
		TemporaryStorage: tempStorage,
		WebHookServer:    localServer,
	}
	engineProvider := engines.Engines()["mock"]
	engine, err := engineProvider.NewEngine(engines.EngineOptions{
		Environment: environment,
		Log:         logger.WithField("engine", "mock"),
	})
	if err != nil {
		t.Fatal(err.Error())
	}

	cfg := &configType{
		QueueBaseURL: serverURL,
	}
	tm, err := newTaskManager(cfg, engine, MockPlugin{}, environment, logger.WithField("test", "TestRunTask"), gc)
	if err != nil {
		t.Fatal(err)
	}

	claims := []*taskClaim{
		&taskClaim{
			taskID: "abc",
			runID:  1,
			definition: &queue.TaskDefinitionResponse{
				Payload: []byte(`{"delay": 5000,"function": "write-log","argument": "Hello World"}`),
			},
			taskClaim: &queue.TaskClaimResponse{
				Credentials: struct {
					AccessToken string `json:"accessToken"`
					Certificate string `json:"certificate"`
					ClientID    string `json:"clientId"`
				}{
					AccessToken: "123",
					ClientID:    "abc",
					Certificate: "",
				},
				TakenUntil: tcclient.Time(time.Now().Add(time.Minute * 5)),
			},
		},
		&taskClaim{
			taskID: "def",
			runID:  0,
			definition: &queue.TaskDefinitionResponse{
				Payload: []byte(`{"delay": 5000,"function": "write-log","argument": "Hello World"}`),
			},
			taskClaim: &queue.TaskClaimResponse{
				Credentials: struct {
					AccessToken string `json:"accessToken"`
					Certificate string `json:"certificate"`
					ClientID    string `json:"clientId"`
				}{
					AccessToken: "123",
					ClientID:    "abc",
					Certificate: "",
				},
				TakenUntil: tcclient.Time(time.Now().Add(time.Minute * 5)),
			},
		},
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		for _, c := range claims {
			go func(claim *taskClaim) {
				tm.run(claim)
				wg.Done()
			}(c)
		}
	}()

	time.Sleep(500 * time.Millisecond)
	assert.Equal(t, len(tm.RunningTasks()), 2)
	close(tm.done)
	tm.Stop()

	wg.Wait()
	assert.Equal(t, 0, len(tm.RunningTasks()))
	assert.Equal(t, int32(2), atomic.LoadInt32(&resCount))
}
func TestMetaServiceShell(t *testing.T) {
	// Create temporary storage
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}

	// Setup a new MetaService
	log := bytes.NewBuffer(nil)
	resolved := false
	meta := New([]string{"bash", "-c", "whoami"}, make(map[string]string), log, func(r bool) {
		if resolved {
			panic("It shouldn't be possible to resolve twice")
		}
		resolved = true
	}, &runtime.Environment{
		TemporaryStorage: storage,
	})
	s := httptest.NewServer(meta)
	defer s.Close()

	debug("### Test shell running an echo service")

	go func() {
		// Start polling for an action
		req, err2 := http.NewRequest("GET", s.URL+"/engine/v1/poll", nil)
		nilOrPanic(err2)
		res, err2 := http.DefaultClient.Do(req)
		nilOrPanic(err2)
		assert(res.StatusCode == http.StatusOK)
		action := Action{}
		data, err2 := ioutil.ReadAll(res.Body)
		nilOrPanic(err2)
		err2 = json.Unmarshal(data, &action)
		nilOrPanic(err2, "Failed to decode JSON")

		// Check that the action is 'exec-shell' (as expected)
		assert(action.ID != "", "Expected action.ID != ''")
		assert(action.Type == "exec-shell", "Expected exec-shell action")
		assert(action.Path == "", "Didn't expect action.Path")

		dialer := websocket.Dialer{
			HandshakeTimeout: 30 * time.Second,
			ReadBufferSize:   8 * 1024,
			WriteBufferSize:  8 * 1024,
		}
		ws, _, err2 := dialer.Dial("ws:"+s.URL[5:]+"/engine/v1/reply?id="+action.ID, nil)
		nilOrPanic(err2, "Failed to open websocket")

		debug("guest-tool: Read: 'hi' on stdin")
		t, m, err2 := ws.ReadMessage()
		nilOrPanic(err2, "ReadMessage failed")
		assert(t == websocket.BinaryMessage, "expected BinaryMessage")
		assert(bytes.Compare(m, []byte{
			shellconsts.MessageTypeData, shellconsts.StreamStdin, 'h', 'i',
		}) == 0, "expected 'hi' on stdin")

		debug("guest-tool: Ack: 'hi' from stdin")
		err2 = ws.WriteMessage(websocket.BinaryMessage, []byte{
			shellconsts.MessageTypeAck, shellconsts.StreamStdin, 0, 0, 0, 2,
		})
		nilOrPanic(err2, "Failed to send ack")

		debug("guest-tool: Send: 'hello' on stdout")
		err2 = ws.WriteMessage(websocket.BinaryMessage, []byte{
			shellconsts.MessageTypeData, shellconsts.StreamStdout, 'h', 'e', 'l', 'l', 'o',
		})
		nilOrPanic(err2, "Failed to send 'hello'")

		debug("guest-tool: Read: ack for the 'hello'")
		t, m, err2 = ws.ReadMessage()
		nilOrPanic(err2, "Failed to ReadMessage")
		assert(t == websocket.BinaryMessage, "expected BinaryMessage")
		assert(bytes.Compare(m, []byte{
			shellconsts.MessageTypeAck, shellconsts.StreamStdout, 0, 0, 0, 5,
		}) == 0, "expected ack for 5 on stdout")

		debug("guest-tool: Send: close on stdout")
		err2 = ws.WriteMessage(websocket.BinaryMessage, []byte{
			shellconsts.MessageTypeData, shellconsts.StreamStdout,
		})
		nilOrPanic(err2, "Failed to send close for stdout")

		debug("guest-tool: Read: close for stdin")
		t, m, err2 = ws.ReadMessage()
		nilOrPanic(err2, "Failed to ReadMessage")
		assert(t == websocket.BinaryMessage, "expected BinaryMessage")
		assert(bytes.Compare(m, []byte{
			shellconsts.MessageTypeData, shellconsts.StreamStdin,
		}) == 0, "expected stdin to be closed")

		debug("guest-tool: Send: exit success")
		err2 = ws.WriteMessage(websocket.BinaryMessage, []byte{
			shellconsts.MessageTypeExit, 0,
		})
		nilOrPanic(err2, "Failed to send 'exit' success")
	}()

	// Exec shell through metaservice
	shell, err := meta.ExecShell(nil, false)
	assert(err == nil, "Unexpected error: ", err)

	debug("server: Writing 'hi' on stdin")
	_, err = shell.StdinPipe().Write([]byte("hi"))
	nilOrPanic(err, "Failed to write 'hi' on stdin")

	debug("server: Reading stdout (waiting for stdout to close)")
	b, err := ioutil.ReadAll(shell.StdoutPipe())
	nilOrPanic(err, "Failed to readAll from stdout")
	assert(string(b) == "hello", "Failed to read 'hello'")

	debug("server: Closing stdin")
	err = shell.StdinPipe().Close()
	nilOrPanic(err)

	debug("server: Waiting for exit success")
	success, err := shell.Wait()
	assert(err == nil, "Unexpected error: ", err)
	assert(success, "Expected success")

	debug("server: Reading nothing stderr (just check that it's closed)")
	b, err = ioutil.ReadAll(shell.StderrPipe())
	nilOrPanic(err, "Failed to readAll from stderr")
	assert(string(b) == "", "Failed to read ''")
}
func TestMetaService(t *testing.T) {
	// Create temporary storage
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}

	// Setup a new MetaService
	log := bytes.NewBuffer(nil)
	result := false
	resolved := false
	s := New([]string{"bash", "-c", "whoami"}, make(map[string]string), log, func(r bool) {
		if resolved {
			panic("It shouldn't be possible to resolve twice")
		}
		resolved = true
		result = r
	}, &runtime.Environment{
		TemporaryStorage: storage,
	})

	// Upload some log
	req, err := http.NewRequest("POST", "http://169.254.169.254/engine/v1/log", bytes.NewBufferString("Hello World"))
	nilOrPanic(err)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	assert(w.Code == http.StatusOK)

	// Check the log
	if log.String() != "Hello World" {
		panic("Expected 'Hello World' in the log")
	}

	// Check that we can report success
	req, err = http.NewRequest("PUT", "http://169.254.169.254/engine/v1/success", nil)
	nilOrPanic(err)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	assert(w.Code == http.StatusOK)

	// Check result
	assert(resolved)
	assert(result)

	// Check idempotency
	req, err = http.NewRequest("PUT", "http://169.254.169.254/engine/v1/success", nil)
	nilOrPanic(err)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	assert(w.Code == http.StatusOK)

	// Check that we can have a conflict
	req, err = http.NewRequest("PUT", "http://169.254.169.254/engine/v1/failed", nil)
	nilOrPanic(err)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	assert(w.Code == http.StatusConflict)

	debug("### Test polling and get-artifact")

	// Check that we can poll for an action, and reply with an artifact
	go func() {
		// Start polling for an action
		req, err2 := http.NewRequest("GET", "http://169.254.169.254/engine/v1/poll", nil)
		nilOrPanic(err2)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
		action := Action{}
		err2 = json.Unmarshal(w.Body.Bytes(), &action)
		nilOrPanic(err2, "Failed to decode JSON")

		// Check that the action is 'get-artifact' (as expected)
		assert(action.ID != "", "Expected action.ID != ''")
		assert(action.Type == "get-artifact", "Expected get-artifact action")
		assert(action.Path == "/home/worker/test-file", "Expected action.Path")

		// Post back an artifact
		req, err2 = http.NewRequest("POST",
			"http://169.254.169.254/engine/v1/reply?id="+action.ID,
			bytes.NewBufferString("hello-world"),
		)
		nilOrPanic(err2)
		w = httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
	}()

	// Get artifact through metaservice
	f, err := s.GetArtifact("/home/worker/test-file")
	nilOrPanic(err, "Failed to get artifact")
	defer f.Close()
	b, err := ioutil.ReadAll(f)
	nilOrPanic(err, "Error reading from file")
	assert(string(b) == "hello-world", "Expected hello-world artifact")

	debug("### Test polling and get-artifact for non-existing file")

	// Check that we can poll for an action, and reply with an error not-found
	go func() {
		// Start polling for an action
		req, err2 := http.NewRequest("GET", "http://169.254.169.254/engine/v1/poll", nil)
		nilOrPanic(err2)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
		action := Action{}
		err2 = json.Unmarshal(w.Body.Bytes(), &action)
		nilOrPanic(err2, "Failed to decode JSON")

		// Check that the action is 'get-artifact' (as expected)
		assert(action.ID != "", "Expected action.ID != ''")
		assert(action.Type == "get-artifact", "Expected get-artifact action")
		assert(action.Path == "/home/worker/wrong-file", "Expected action.Path")

		// Post back an artifact
		req, err2 = http.NewRequest("POST",
			"http://169.254.169.254/engine/v1/reply?id="+action.ID,
			nil,
		)
		nilOrPanic(err2)
		req.Header.Set("X-Taskcluster-Worker-Error", "file-not-found")
		w = httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
	}()

	// Get error for artifact through metaservice
	f, err = s.GetArtifact("/home/worker/wrong-file")
	assert(f == nil, "Didn't expect to get a file")
	assert(err == engines.ErrResourceNotFound, "Expected ErrResourceNotFound")

	debug("### Test polling and list-folder")

	// Check that we can poll for an action, and reply to a list-folder
	go func() {
		// Start polling for an action
		req, err2 := http.NewRequest("GET", "http://169.254.169.254/engine/v1/poll", nil)
		nilOrPanic(err2)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
		action := Action{}
		err2 = json.Unmarshal(w.Body.Bytes(), &action)
		nilOrPanic(err2, "Failed to decode JSON")

		// Check that the action is 'list-folder' (as expected)
		assert(action.ID != "", "Expected action.ID != ''")
		assert(action.Type == "list-folder", "Expected list-folder action")
		assert(action.Path == "/home/worker/", "Expected action.Path")

		// Post back an reply
		payload, _ := json.Marshal(Files{
			Files:    []string{"/home/worker/test-file"},
			NotFound: false,
		})
		req, err2 = http.NewRequest("POST",
			"http://169.254.169.254/engine/v1/reply?id="+action.ID,
			bytes.NewBuffer(payload),
		)
		nilOrPanic(err2)
		req.Header.Set("Content-Type", "application/json")
		w = httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK, "Unexpected status: ", w.Code)
	}()

	// List folder through metaservice
	files, err := s.ListFolder("/home/worker/")
	nilOrPanic(err, "Failed to list-folder")
	assert(len(files) == 1, "Expected one file")
	assert(files[0] == "/home/worker/test-file", "Got the wrong file")

	debug("### Test polling and list-folder (not-found)")

	// Check that we can poll for an action, and reply to a list-folder, not found
	go func() {
		// Start polling for an action
		req, err2 := http.NewRequest("GET", "http://169.254.169.254/engine/v1/poll", nil)
		nilOrPanic(err2)
		w := httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK)
		action := Action{}
		err2 = json.Unmarshal(w.Body.Bytes(), &action)
		nilOrPanic(err2, "Failed to decode JSON")

		// Check that the action is 'list-folder' (as expected)
		assert(action.ID != "", "Expected action.ID != ''")
		assert(action.Type == "list-folder", "Expected list-folder action")
		assert(action.Path == "/home/worker/missing/", "Expected action.Path")

		// Post back an reply
		payload, _ := json.Marshal(Files{
			Files:    nil,
			NotFound: true,
		})
		req, err2 = http.NewRequest("POST",
			"http://169.254.169.254/engine/v1/reply?id="+action.ID,
			bytes.NewBuffer(payload),
		)
		nilOrPanic(err2)
		req.Header.Set("Content-Type", "application/json")
		w = httptest.NewRecorder()
		s.ServeHTTP(w, req)
		assert(w.Code == http.StatusOK, "Unexpected status: ", w.Code)
	}()

	// List folder through metaservice
	files, err = s.ListFolder("/home/worker/missing/")
	assert(err == engines.ErrResourceNotFound, "Expected ErrResourceNotFound")
	assert(len(files) == 0, "Expected zero files")
}
func TestGuestToolsLiveLog(t *testing.T) {
	nowReady := sync.WaitGroup{}
	nowReady.Add(1)
	ps := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		debug("Waiting for ready-now to be readable in log")
		nowReady.Wait()
		debug("replying: request-ok")

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("request-ok"))
	}))

	// Create temporary storage
	storage, err := runtime.NewTemporaryStorage(os.TempDir())
	if err != nil {
		panic("Failed to create TemporaryStorage")
	}
	environment := &runtime.Environment{
		TemporaryStorage: storage,
	}

	// Setup a new MetaService
	reader, writer := io.Pipe()
	result := false
	resolved := false
	m := sync.Mutex{}
	s := metaservice.New([]string{"sh", "-c", "echo \"$TEST_TEXT\" && curl -s " + ps.URL}, map[string]string{
		"TEST_TEXT": "ready-now",
	}, writer, func(r bool) {
		m.Lock()
		defer m.Unlock()
		if resolved {
			panic("It shouldn't be possible to resolve twice")
		}
		resolved = true
		result = r
	}, environment)

	// Create http server for testing
	ts := httptest.NewServer(s)
	defer ts.Close()
	u, err := url.Parse(ts.URL)
	if err != nil {
		panic("Expected a url we can parse")
	}

	// Create a logger
	logger, _ := runtime.CreateLogger("info")
	log := logger.WithField("component", "guest-tools-tests")

	// Wait for
	logTask := bytes.NewBuffer(nil)
	logDone := sync.WaitGroup{}
	logDone.Add(1)
	go func() {
		b := make([]byte, 1)
		for !strings.Contains(logTask.String(), "ready-now") {
			n, err := reader.Read(b)
			logTask.Write(b[:n])
			if err != nil {
				panic("Unexpected error")
			}
		}
		nowReady.Done()
		io.Copy(logTask, reader)
		logDone.Done()
	}()

	// Create an run guest-tools
	g := new(u.Host, log)
	g.Run()
	writer.Close()
	logDone.Wait()

	// Check the state
	if !resolved {
		t.Error("Expected the metadata to have resolved the task")
	}
	if result != true {
		t.Error("Expected the metadata to get successful result")
	}
	if !strings.Contains(logTask.String(), "request-ok") {
		t.Error("Got unexpected taskLog: '", logTask.String(), "'")
	}
}