Ctrl K
ComunidadGuías…

iclient/incus_console_test.go

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package iclient

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/lxc/incus/v7/shared/api"
	"github.com/stretchr/testify/require"
)

// TestIncusConsoleInstanceRefusesWithoutOutput: there is nowhere to put the
// stream, so this is refused rather than opening sockets and dropping it.
func TestIncusConsoleInstanceRefusesWithoutOutput(t *testing.T) {
	t.Parallel()

	conn, seen := recordingServer(t, `{}`)

	_, err := conn.ConsoleInstance(t.Context(), "myproject", "web-1", api.InstanceConsolePost{}, nil)
	require.Error(t, err)
	require.Empty(t, seen.all(), "it must refuse before asking the server")

	_, err = conn.ConsoleInstance(t.Context(), "myproject", "web-1", api.InstanceConsolePost{}, &InstanceConsoleArgs{})
	require.Error(t, err)
	require.Empty(t, seen.all())
}

// rawServer answers with a body that is not an API envelope, the way the
// console log endpoint does.
func rawServer(t *testing.T, status int, body string) *Connection {
	t.Helper()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(status)

		_, _ = io.WriteString(w, body)
	}))
	t.Cleanup(server.Close)

	conn, err := NewConnection(&ConfigRemoteInfo{
		Name:  "raw",
		Addrs: []string{server.URL},
	})
	require.NoError(t, err)

	return conn
}

// TestIncusGetInstanceConsoleLogRaw: the console log is plain bytes, so it must
// come back undecoded rather than through the envelope reader.
func TestIncusGetInstanceConsoleLogRaw(t *testing.T) {
	t.Parallel()

	conn := rawServer(t, http.StatusOK, "line one\nline two\n")

	reader, err := conn.GetInstanceConsoleLog(t.Context(), "myproject", "web-1")
	require.NoError(t, err)

	defer func() { _ = reader.Close() }()

	body, err := io.ReadAll(reader)
	require.NoError(t, err)
	require.Equal(t, "line one\nline two\n", string(body))
}

// TestIncusGetInstanceConsoleLogError: a failure still answers with an envelope,
// so it has to reach the caller as the same StatusError any other call produces.
func TestIncusGetInstanceConsoleLogError(t *testing.T) {
	t.Parallel()

	conn := rawServer(t, http.StatusNotFound,
		`{"type":"error","error_code":404,"error":"Instance not found"}`)

	_, err := conn.GetInstanceConsoleLog(t.Context(), "myproject", "nope")
	require.Error(t, err)
	require.True(t, api.StatusErrorCheck(err, 404), "want a 404 StatusError, got %v", err)
	require.Contains(t, err.Error(), "Instance not found")
}

func TestIncusGetInstanceConsoleLogURL(t *testing.T) {
	t.Parallel()

	conn, seen := recordingServer(t, `{}`)

	reader, err := conn.GetInstanceConsoleLog(t.Context(), "myproject", "web-1")
	require.NoError(t, err)
	_ = reader.Close()

	require.Equal(t, []string{"/1.0/instances/web-1/console?project=myproject"}, seen.uris())
}

// TestIncusConsoleInstanceAgainstRealIncus reads a running instance's console,
// which is what `incus-compose logs` is built on.
func TestIncusConsoleInstanceAgainstRealIncus(t *testing.T) {
	skipE2E(t)
	t.Parallel()

	ctx := t.Context()
	conn := testConnection(t)

	project := testProject(t, conn, "iclient-console")

	const name = "console-1"

	// Something that keeps writing, so the assertion below cannot pass on an
	// instance that produced nothing.
	testInstance(t, conn, project, name, map[string]string{
		"oci.entrypoint": "sh -c 'while true; do echo iclient-console-marker; sleep 1; done'",
	})

	// The buffer holds whatever was written before the attach, so this is the
	// same stream read two ways.
	reader, err := conn.GetInstanceConsoleLog(ctx, project, name)
	require.NoError(t, err)

	_, err = io.ReadAll(reader)
	require.NoError(t, err)
	require.NoError(t, reader.Close())

	output := &syncBuffer{}

	attachCtx, detach := context.WithTimeout(ctx, 30*time.Second)
	defer detach()

	updates, err := conn.ConsoleInstance(attachCtx, project, name, api.InstanceConsolePost{Force: true},
		&InstanceConsoleArgs{Output: output})
	require.NoError(t, err)

	require.Eventually(t, func() bool {
		return strings.Contains(output.String(), "iclient-console-marker")
	}, 20*time.Second, 250*time.Millisecond, "the console stream carried nothing")

	detach()

	_, _ = WaitOperation(context.Background(), updates)
}

// syncBuffer collects the console stream, which is written from the drain
// goroutine while the test reads it.
type syncBuffer struct {
	mu   sync.Mutex
	data []byte
}

func (b *syncBuffer) Write(p []byte) (int, error) {
	b.mu.Lock()
	defer b.mu.Unlock()

	b.data = append(b.data, p...)

	return len(p), nil
}

func (b *syncBuffer) String() string {
	b.mu.Lock()
	defer b.mu.Unlock()

	return string(b.data)
}