2022-10-13 16:01:23 +02:00
|
|
|
package hsic
|
|
|
|
|
|
|
|
import (
|
2022-11-02 09:55:09 +01:00
|
|
|
"archive/tar"
|
|
|
|
"bytes"
|
2022-10-13 16:01:23 +02:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-11-02 09:55:09 +01:00
|
|
|
"io"
|
2022-10-13 16:01:23 +02:00
|
|
|
"log"
|
|
|
|
"net/http"
|
2022-11-02 09:55:09 +01:00
|
|
|
"path/filepath"
|
2022-10-13 16:01:23 +02:00
|
|
|
|
|
|
|
"github.com/juanfont/headscale"
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
|
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
|
|
"github.com/ory/dockertest/v3"
|
2022-11-02 09:55:09 +01:00
|
|
|
"github.com/ory/dockertest/v3/docker"
|
2022-10-13 16:01:23 +02:00
|
|
|
)
|
|
|
|
|
2022-10-18 12:09:10 +02:00
|
|
|
const (
|
|
|
|
hsicHashLength = 6
|
|
|
|
dockerContextPath = "../."
|
2022-11-02 11:08:54 +01:00
|
|
|
aclPolicyPath = "/etc/headscale/acl.hujson"
|
2022-10-18 12:09:10 +02:00
|
|
|
)
|
2022-10-13 16:01:23 +02:00
|
|
|
|
|
|
|
var errHeadscaleStatusCodeNotOk = errors.New("headscale status code not ok")
|
|
|
|
|
|
|
|
type HeadscaleInContainer struct {
|
|
|
|
hostname string
|
|
|
|
port int
|
|
|
|
|
|
|
|
pool *dockertest.Pool
|
|
|
|
container *dockertest.Resource
|
|
|
|
network *dockertest.Network
|
2022-11-02 11:08:54 +01:00
|
|
|
|
|
|
|
// optional config
|
|
|
|
aclPolicy *headscale.ACLPolicy
|
|
|
|
env []string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Option = func(c *HeadscaleInContainer)
|
|
|
|
|
|
|
|
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|
|
|
return func(hsic *HeadscaleInContainer) {
|
|
|
|
hsic.aclPolicy = acl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithConfigEnv(configEnv map[string]string) Option {
|
|
|
|
return func(hsic *HeadscaleInContainer) {
|
|
|
|
env := []string{}
|
|
|
|
|
|
|
|
for key, value := range configEnv {
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
|
|
|
}
|
|
|
|
|
|
|
|
hsic.env = env
|
|
|
|
}
|
2022-10-13 16:01:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func New(
|
|
|
|
pool *dockertest.Pool,
|
|
|
|
port int,
|
2022-10-18 12:09:10 +02:00
|
|
|
network *dockertest.Network,
|
2022-11-02 11:08:54 +01:00
|
|
|
opts ...Option,
|
2022-10-18 12:09:10 +02:00
|
|
|
) (*HeadscaleInContainer, error) {
|
2022-10-13 16:01:23 +02:00
|
|
|
hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-11-02 11:08:54 +01:00
|
|
|
hostname := fmt.Sprintf("hs-%s", hash)
|
|
|
|
portProto := fmt.Sprintf("%d/tcp", port)
|
|
|
|
|
|
|
|
hsic := &HeadscaleInContainer{
|
|
|
|
hostname: hostname,
|
|
|
|
port: port,
|
|
|
|
|
|
|
|
pool: pool,
|
|
|
|
network: network,
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(hsic)
|
|
|
|
}
|
|
|
|
|
|
|
|
if hsic.aclPolicy != nil {
|
|
|
|
hsic.env = append(hsic.env, fmt.Sprintf("HEADSCALE_ACL_POLICY_PATH=%s", aclPolicyPath))
|
|
|
|
}
|
|
|
|
|
2022-10-13 16:01:23 +02:00
|
|
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
2022-11-02 09:53:55 +01:00
|
|
|
Dockerfile: "Dockerfile.debug",
|
2022-10-13 16:01:23 +02:00
|
|
|
ContextDir: dockerContextPath,
|
|
|
|
}
|
|
|
|
|
|
|
|
runOptions := &dockertest.RunOptions{
|
2022-11-02 09:55:48 +01:00
|
|
|
Name: hostname,
|
2022-10-13 16:01:23 +02:00
|
|
|
ExposedPorts: []string{portProto},
|
2022-11-02 09:55:48 +01:00
|
|
|
Networks: []*dockertest.Network{network},
|
|
|
|
// Cmd: []string{"headscale", "serve"},
|
|
|
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
|
|
|
// to inject the headscale configuration further down.
|
|
|
|
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"},
|
2022-11-02 11:08:54 +01:00
|
|
|
Env: hsic.env,
|
2022-10-13 16:01:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// dockertest isnt very good at handling containers that has already
|
|
|
|
// been created, this is an attempt to make sure this container isnt
|
|
|
|
// present.
|
|
|
|
err = pool.RemoveContainerByName(hostname)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
container, err := pool.BuildAndRunWithBuildOptions(
|
|
|
|
headscaleBuildOptions,
|
|
|
|
runOptions,
|
|
|
|
dockertestutil.DockerRestartPolicy,
|
|
|
|
dockertestutil.DockerAllowLocalIPv6,
|
|
|
|
dockertestutil.DockerAllowNetworkAdministration,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not start headscale container: %w", err)
|
|
|
|
}
|
|
|
|
log.Printf("Created %s container\n", hostname)
|
|
|
|
|
2022-11-02 11:08:54 +01:00
|
|
|
hsic.container = container
|
2022-11-02 09:55:48 +01:00
|
|
|
|
|
|
|
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(DefaultConfigYAML()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-11-02 11:08:54 +01:00
|
|
|
if hsic.aclPolicy != nil {
|
|
|
|
data, err := json.Marshal(hsic.aclPolicy)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = hsic.WriteFile(aclPolicyPath, data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to write ACL policy to container: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-02 09:55:48 +01:00
|
|
|
return hsic, nil
|
2022-10-13 16:01:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) Shutdown() error {
|
|
|
|
return t.pool.Purge(t.container)
|
|
|
|
}
|
|
|
|
|
2022-10-24 16:40:49 +02:00
|
|
|
func (t *HeadscaleInContainer) Execute(
|
|
|
|
command []string,
|
|
|
|
) (string, error) {
|
|
|
|
log.Println("command", command)
|
|
|
|
log.Printf("running command for %s\n", t.hostname)
|
|
|
|
stdout, stderr, err := dockertestutil.ExecuteCommand(
|
|
|
|
t.container,
|
|
|
|
command,
|
|
|
|
[]string{},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("command stderr: %s\n", stderr)
|
|
|
|
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if stdout != "" {
|
|
|
|
log.Printf("command stdout: %s\n", stdout)
|
|
|
|
}
|
|
|
|
|
|
|
|
return stdout, nil
|
|
|
|
}
|
|
|
|
|
2022-10-13 16:01:23 +02:00
|
|
|
func (t *HeadscaleInContainer) GetIP() string {
|
|
|
|
return t.container.GetIPInNetwork(t.network)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) GetPort() string {
|
|
|
|
portProto := fmt.Sprintf("%d/tcp", t.port)
|
|
|
|
|
|
|
|
return t.container.GetPort(portProto)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
2022-10-18 11:58:15 +02:00
|
|
|
hostEndpoint := fmt.Sprintf("%s:%d",
|
2022-10-13 16:01:23 +02:00
|
|
|
t.GetIP(),
|
2022-10-18 11:58:15 +02:00
|
|
|
t.port)
|
2022-10-13 16:01:23 +02:00
|
|
|
|
|
|
|
return fmt.Sprintf("http://%s/health", hostEndpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) GetEndpoint() string {
|
2022-10-18 11:58:15 +02:00
|
|
|
hostEndpoint := fmt.Sprintf("%s:%d",
|
2022-10-13 16:01:23 +02:00
|
|
|
t.GetIP(),
|
2022-10-18 11:58:15 +02:00
|
|
|
t.port)
|
2022-10-13 16:01:23 +02:00
|
|
|
|
|
|
|
return fmt.Sprintf("http://%s", hostEndpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) WaitForReady() error {
|
|
|
|
url := t.GetHealthEndpoint()
|
|
|
|
|
2022-10-18 11:58:15 +02:00
|
|
|
log.Printf("waiting for headscale to be ready at %s", url)
|
|
|
|
|
2022-10-13 16:01:23 +02:00
|
|
|
return t.pool.Retry(func() error {
|
2022-10-18 12:19:43 +02:00
|
|
|
resp, err := http.Get(url) //nolint
|
2022-10-13 16:01:23 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("headscale is not ready: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return errHeadscaleStatusCodeNotOk
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) CreateNamespace(
|
|
|
|
namespace string,
|
|
|
|
) error {
|
|
|
|
command := []string{"headscale", "namespaces", "create", namespace}
|
|
|
|
|
|
|
|
_, _, err := dockertestutil.ExecuteCommand(
|
|
|
|
t.container,
|
|
|
|
command,
|
|
|
|
[]string{},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) CreateAuthKey(
|
|
|
|
namespace string,
|
|
|
|
) (*v1.PreAuthKey, error) {
|
|
|
|
command := []string{
|
|
|
|
"headscale",
|
|
|
|
"--namespace",
|
|
|
|
namespace,
|
|
|
|
"preauthkeys",
|
|
|
|
"create",
|
|
|
|
"--reusable",
|
|
|
|
"--expiration",
|
|
|
|
"24h",
|
|
|
|
"--output",
|
|
|
|
"json",
|
|
|
|
}
|
|
|
|
|
|
|
|
result, _, err := dockertestutil.ExecuteCommand(
|
|
|
|
t.container,
|
|
|
|
command,
|
|
|
|
[]string{},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to execute create auth key command: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var preAuthKey v1.PreAuthKey
|
|
|
|
err = json.Unmarshal([]byte(result), &preAuthKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal auth key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &preAuthKey, nil
|
|
|
|
}
|
|
|
|
|
2022-10-23 12:41:35 +02:00
|
|
|
func (t *HeadscaleInContainer) ListMachinesInNamespace(
|
2022-10-13 16:01:23 +02:00
|
|
|
namespace string,
|
|
|
|
) ([]*v1.Machine, error) {
|
|
|
|
command := []string{"headscale", "--namespace", namespace, "nodes", "list", "--output", "json"}
|
|
|
|
|
|
|
|
result, _, err := dockertestutil.ExecuteCommand(
|
|
|
|
t.container,
|
|
|
|
command,
|
|
|
|
[]string{},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to execute list node command: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var nodes []*v1.Machine
|
|
|
|
err = json.Unmarshal([]byte(result), &nodes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal nodes: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes, nil
|
|
|
|
}
|
2022-11-02 09:55:09 +01:00
|
|
|
|
|
|
|
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
|
|
|
dirPath, fileName := filepath.Split(path)
|
|
|
|
|
|
|
|
file := bytes.NewReader(data)
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer([]byte{})
|
|
|
|
|
|
|
|
tarWriter := tar.NewWriter(buf)
|
|
|
|
|
|
|
|
header := &tar.Header{
|
|
|
|
Name: fileName,
|
|
|
|
Size: file.Size(),
|
|
|
|
// Mode: int64(stat.Mode()),
|
|
|
|
// ModTime: stat.ModTime(),
|
|
|
|
}
|
|
|
|
|
|
|
|
err := tarWriter.WriteHeader(header)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed write file header to tar: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = io.Copy(tarWriter, file)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to copy file to tar: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tarWriter.Close()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to close tar: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("tar: %s", buf.String())
|
|
|
|
|
|
|
|
// Ensure the directory is present inside the container
|
|
|
|
_, err = t.Execute([]string{"mkdir", "-p", dirPath})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to ensure directory: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = t.pool.Client.UploadToContainer(
|
|
|
|
t.container.Container.ID,
|
|
|
|
docker.UploadToContainerOptions{
|
|
|
|
NoOverwriteDirNonDir: false,
|
|
|
|
Path: dirPath,
|
|
|
|
InputStream: bytes.NewReader(buf.Bytes()),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|