package integrationtest import ( "context" "fmt" "git.wamblee.org/converge/pkg/support/ioutils" "git.wamblee.org/converge/pkg/support/sshsupport" "git.wamblee.org/converge/pkg/testsupport" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" "io" "log" "os" "path/filepath" "strings" "testing" "time" ) const ( agentEntryPointTemplate = ` curl -v http://converge:8000/downloads/agent > agent chmod 755 agent echo "{{.publicKey}}" > .authorized_keys cat .authorized_keys echo "HOSTNAME=$( hostname )" ./agent --id abc ws://converge:8000 tail -f /dev/null ` clientEntryPointTemplate = ` echo "HOSTNAME_OF_AGENT_ON_CLIENT=agent" echo Ready sleep 1000000 ` clientEntryPointTemplatex = ` curl -v http://converge:8000/downloads/wsproxy > wsproxy chmod 755 wsproxy echo "{{.privateKey}}" > id_rsa chmod 400 id_rsa echo "====================================================================================================" echo " hostname | sed 's/^/=============== HOSTNAME_OF_AGENT_ON_CLIENT=/g' " | ssh -tt -oStrictHostKeyChecking=no -i id_rsa -oProxyCommand="./wsproxy ws://converge:8000/client/abc" localhost echo "====================================================================================================" echo Ready sleep 1000000` ) var ( version = "1.0.0" convergeImage = fmt.Sprintf("%s/converge:%s", getRegistry(), version) agentImage = fmt.Sprintf("%s/converge-test:%s", getRegistry(), version) clientImage = agentImage ) func getRegistry() string { registry, ok := os.LookupEnv("REGISTRY") if ok { return registry } moduleDir := testsupport.GetGoModDIr() log.Printf("Go module directory %s", moduleDir) envFile := filepath.Join(moduleDir, ".env") file, err := os.Open(envFile) if err != nil { panic("Could not find .env file and REGISTRY env var is not set") } defer file.Close() props, err := ioutils.ReadPropertyFile(file) if err != nil { panic(fmt.Sprintf("Error reading environment file '%v': %v", envFile, err)) } registry, ok = props["REGISTRY"] if !ok { panic(fmt.Sprintf("Could not find REGISTRY setting in property file '%v'", envFile)) } return registry } type IntegrationTestSuite struct { suite.Suite ctx context.Context cancelFunc context.CancelFunc } func Test_IntegrationTestSuite(t *testing.T) { suite.Run(t, &IntegrationTestSuite{}) } func (s *IntegrationTestSuite) SetupSuite() { } func (s *IntegrationTestSuite) TearDownSuite() { } func (s *IntegrationTestSuite) SetupTest() { ctx, cancelFunc := testsupport.CreateTestContext(context.Background(), 60*time.Second) s.ctx = ctx s.cancelFunc = cancelFunc } func (s *IntegrationTestSuite) TearDownTest() { s.cancelFunc() } type ContainerLogger func(logtype string, msg string) func logContainerOutput(container string, logtype string, msg string) { log.Printf("%s: %s: %s", container, logtype, msg) } func containerLogger(container string) ContainerLogger { return func(logtype string, msg string) { logContainerOutput(container, logtype, msg) } } func (c ContainerLogger) Accept(log testcontainers.Log) { c(log.LogType, string(log.Content)) } func createLogConsumerConfig(container string) *testcontainers.LogConsumerConfig { return &testcontainers.LogConsumerConfig{ Opts: []testcontainers.LogProductionOption{testcontainers.WithLogProductionTimeout(10 * time.Second)}, Consumers: []testcontainers.LogConsumer{containerLogger(container)}, } } func (s *IntegrationTestSuite) runCmd(container testcontainers.Container, cmd []string) (int, string, error) { status, reader, err := container.Exec(s.ctx, cmd) if err != nil { return 0, "", err } output, err := io.ReadAll(reader) if err != nil { return 0, "", err } return status, string(output), nil } func (s *IntegrationTestSuite) runCmdFailOnError(container testcontainers.Container, cmd []string) (int, string) { status, output, err := s.runCmd(container, cmd) s.Require().Nil(err) return status, output } func (s *IntegrationTestSuite) defineContainer(net *testcontainers.DockerNetwork, hostname string, image string, logLine string, ports []string) testcontainers.GenericContainerRequest { log.Printf("PORTS %v", ports) waitStrategies := []wait.Strategy{wait.ForLog(logLine)} if len(ports) > 0 { waitStrategies = append(waitStrategies, wait.ForExposedPort()) } request := testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: image, ExposedPorts: ports, Hostname: hostname, WaitingFor: wait.ForAll(waitStrategies...).WithStartupTimeoutDefault(30 * time.Second), LogConsumerCfg: createLogConsumerConfig(hostname), Networks: []string{net.Name}, //NetworkAliases: map[string][]string{ // net.Name: []string{hostname}, //}, }, Started: false, } return request } func (s *IntegrationTestSuite) Test_SingleAgentAndClient() { net, err := network.New(s.ctx) s.Require().Nil(err) defer net.Remove(s.ctx) // SSH key to use privateKey, publicKey, err := sshsupport.GeneratePrivatePublicKey(2048) s.Require().Nil(err) // The server log.Println("Starting converge server") converge, err := testcontainers.GenericContainer(s.ctx, s.defineContainer(net, "converge", convergeImage, "Rendez-vous server listening", []string{"8000/tcp"})) s.Require().Nil(err) err = converge.Start(s.ctx) s.Require().Nil(err) defer converge.Terminate(s.ctx) port, err := converge.MappedPort(s.ctx, "8000/tcp") s.Require().Nil(err) log.Printf("External port %v", port) // The agent log.Println("Starting agent") agentEntrypoint := testsupport.Template(agentEntryPointTemplate, map[string]string{"publicKey": publicKey}) agentRequest := s.defineContainer(net, "agent", agentImage, "starting ssh server, waiting for debug sessions", []string{}) agentRequest.Entrypoint = []string{ "sh", "-c", agentEntrypoint} agent, err := testcontainers.GenericContainer(s.ctx, agentRequest) s.Require().Nil(err) err = agent.Start(s.ctx) log.Printf("Error from starting agent %+v", err) s.Require().Nil(err) defer agent.Terminate(s.ctx) // The client log.Println("Starting client") clientEntrypoint := testsupport.Template(clientEntryPointTemplate, map[string]string{"privateKey": privateKey}) clientRequest := s.defineContainer(net, "client", clientImage, "Ready", []string{}) clientRequest.Entrypoint = []string{ "sh", "-c", clientEntrypoint} client, err := testcontainers.GenericContainer(s.ctx, clientRequest) s.Require().Nil(err) err = client.Start(s.ctx) log.Printf("Error from starting client %+v", err) s.Require().Nil(err) defer client.Terminate(s.ctx) input, err := client.Logs(s.ctx) s.Require().Nil(err) inputBytes, err := io.ReadAll(input) s.Require().Nil(err) s.True(strings.Contains(string(inputBytes), "HOSTNAME_OF_AGENT_ON_CLIENT=agent")) }