mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
// Package qnap contains dist Targets for building QNAP Tailscale packages.
|
||
|
//
|
||
|
// QNAP dev docs over at https://www.qnap.com/en/how-to/tutorial/article/qpkg-development-guidelines.
|
||
|
package qnap
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"sync"
|
||
|
|
||
|
"tailscale.com/release/dist"
|
||
|
)
|
||
|
|
||
|
type target struct {
|
||
|
goenv map[string]string
|
||
|
arch string
|
||
|
signer *signer
|
||
|
}
|
||
|
|
||
|
type signer struct {
|
||
|
privateKeyPath string
|
||
|
certificatePath string
|
||
|
}
|
||
|
|
||
|
func (t *target) String() string {
|
||
|
return fmt.Sprintf("qnap/%s", t.arch)
|
||
|
}
|
||
|
|
||
|
func (t *target) Build(b *dist.Build) ([]string, error) {
|
||
|
// Stop early if we don't have docker running.
|
||
|
if _, err := exec.LookPath("docker"); err != nil {
|
||
|
return nil, fmt.Errorf("docker not found, cannot build: %w", err)
|
||
|
}
|
||
|
|
||
|
if t.signer != nil {
|
||
|
if err := t.setUpSignatureFiles(b); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
qnapBuilds := getQnapBuilds(b)
|
||
|
inner, err := qnapBuilds.buildInnerPackage(b, t.goenv)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return t.buildQPKG(b, qnapBuilds, inner)
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
qnapTag = "1" // currently static, we don't seem to bump this
|
||
|
)
|
||
|
|
||
|
func (t *target) buildQPKG(b *dist.Build, qnapBuilds *qnapBuilds, inner *innerPkg) ([]string, error) {
|
||
|
if _, err := exec.LookPath("docker"); err != nil {
|
||
|
return nil, fmt.Errorf("docker not found, cannot build: %w", err)
|
||
|
}
|
||
|
|
||
|
if err := qnapBuilds.makeDockerImage(b); err != nil {
|
||
|
return nil, fmt.Errorf("makeDockerImage: %w", err)
|
||
|
}
|
||
|
|
||
|
filename := fmt.Sprintf("Tailscale_%s-%s_%s.qpkg", b.Version.Short, qnapTag, t.arch)
|
||
|
filePath := filepath.Join(b.Out, filename)
|
||
|
|
||
|
cmd := b.Command(b.Repo, "docker", "run", "--rm",
|
||
|
"-e", fmt.Sprintf("ARCH=%s", t.arch),
|
||
|
"-e", fmt.Sprintf("TSTAG=%s", b.Version.Short),
|
||
|
"-e", fmt.Sprintf("QNAPTAG=%s", qnapTag),
|
||
|
"-v", fmt.Sprintf("%s:/tailscale", inner.tailscalePath),
|
||
|
"-v", fmt.Sprintf("%s:/tailscaled", inner.tailscaledPath),
|
||
|
// Tailscale folder has QNAP package setup files needed for building.
|
||
|
"-v", fmt.Sprintf("%s:/Tailscale", filepath.Join(b.Repo, "release/dist/qnap/Tailscale")),
|
||
|
"-v", fmt.Sprintf("%s:/build-qpkg.sh", filepath.Join(b.Repo, "release/dist/qnap/build-qpkg.sh")),
|
||
|
"-v", fmt.Sprintf("%s:/out", b.Out),
|
||
|
"build.tailscale.io/qdk:latest",
|
||
|
"/build-qpkg.sh",
|
||
|
)
|
||
|
|
||
|
// dist.Build runs target builds in parallel goroutines by default.
|
||
|
// For QNAP, this is an issue because the underlaying qbuild builder will
|
||
|
// create tmp directories in the shared docker image that end up conflicting
|
||
|
// with one another.
|
||
|
// So we use a mutex to only allow one "docker run" at a time.
|
||
|
qnapBuilds.dockerImageMu.Lock()
|
||
|
defer qnapBuilds.dockerImageMu.Unlock()
|
||
|
|
||
|
log.Printf("Building %s", filePath)
|
||
|
out, err := cmd.CombinedOutput()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("docker run %v: %s", err, out)
|
||
|
}
|
||
|
|
||
|
return []string{filePath, filePath + ".md5"}, nil
|
||
|
}
|
||
|
|
||
|
func (t *target) setUpSignatureFiles(b *dist.Build) error {
|
||
|
return b.Once(fmt.Sprintf("qnap-signature-%s-%s", t.signer.privateKeyPath, t.signer.certificatePath), func() error {
|
||
|
log.Print("Setting up qnap signature files")
|
||
|
|
||
|
key, err := os.ReadFile(t.signer.privateKeyPath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
cert, err := os.ReadFile(t.signer.certificatePath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// QNAP's qbuild command expects key and cert files to be in the root
|
||
|
// of the project directory (in our case release/dist/qnap/Tailscale).
|
||
|
// So here, we copy the key and cert over to the project folder for the
|
||
|
// duration of qnap package building and then delete them on close.
|
||
|
|
||
|
keyPath := filepath.Join(b.Repo, "release/dist/qnap/Tailscale/private_key")
|
||
|
if err := os.WriteFile(keyPath, key, 0400); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
certPath := filepath.Join(b.Repo, "release/dist/qnap/Tailscale/certificate")
|
||
|
if err := os.WriteFile(certPath, cert, 0400); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b.AddOnCloseFunc(func() error {
|
||
|
return errors.Join(os.Remove(keyPath), os.Remove(certPath))
|
||
|
})
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
type qnapBuildsMemoizeKey struct{}
|
||
|
|
||
|
type innerPkg struct {
|
||
|
tailscalePath string
|
||
|
tailscaledPath string
|
||
|
}
|
||
|
|
||
|
// qnapBuilds holds extra build context shared by all qnap builds.
|
||
|
type qnapBuilds struct {
|
||
|
// innerPkgs contains per-goenv compiled binary paths.
|
||
|
// It is used to avoid repeated compilations for the same architecture.
|
||
|
innerPkgs dist.Memoize[*innerPkg]
|
||
|
dockerImageMu sync.Mutex
|
||
|
}
|
||
|
|
||
|
// getQnapBuilds returns the qnapBuilds for b, creating one if needed.
|
||
|
func getQnapBuilds(b *dist.Build) *qnapBuilds {
|
||
|
return b.Extra(qnapBuildsMemoizeKey{}, func() any { return new(qnapBuilds) }).(*qnapBuilds)
|
||
|
}
|
||
|
|
||
|
// buildInnerPackage builds the go binaries used for qnap packages.
|
||
|
// These binaries get embedded with Tailscale package metadata to form qnap
|
||
|
// releases.
|
||
|
func (m *qnapBuilds) buildInnerPackage(b *dist.Build, goenv map[string]string) (*innerPkg, error) {
|
||
|
return m.innerPkgs.Do(goenv, func() (*innerPkg, error) {
|
||
|
if err := b.BuildWebClientAssets(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", goenv)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", goenv)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// The go binaries above get built and put into a /tmp directory created
|
||
|
// by b.TmpDir(). But, we build QNAP with docker, which doesn't always
|
||
|
// allow for mounting tmp directories (seemingly dependent on docker
|
||
|
// host).
|
||
|
// https://stackoverflow.com/questions/65267251/docker-bind-mount-directory-in-tmp-not-working
|
||
|
//
|
||
|
// So here, we move the binaries into a directory within the b.Repo
|
||
|
// path and clean it up when the builder closes.
|
||
|
|
||
|
tmpDir := filepath.Join(b.Repo, fmt.Sprintf("/tmp-qnap-%s-%s-%s", b.Version.Short, goenv["GOOS"], goenv["GOARCH"]))
|
||
|
if err = os.MkdirAll(tmpDir, 0755); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
b.AddOnCloseFunc(func() error {
|
||
|
return os.RemoveAll(tmpDir)
|
||
|
})
|
||
|
|
||
|
tsBytes, err := os.ReadFile(ts)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
tsdBytes, err := os.ReadFile(tsd)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
tsPath := filepath.Join(tmpDir, "tailscale")
|
||
|
if err := os.WriteFile(tsPath, tsBytes, 0755); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
tsdPath := filepath.Join(tmpDir, "tailscaled")
|
||
|
if err := os.WriteFile(tsdPath, tsdBytes, 0755); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &innerPkg{tailscalePath: tsPath, tailscaledPath: tsdPath}, nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (m *qnapBuilds) makeDockerImage(b *dist.Build) error {
|
||
|
return b.Once("make-qnap-docker-image", func() error {
|
||
|
log.Printf("Building qnapbuilder docker image")
|
||
|
|
||
|
cmd := b.Command(b.Repo, "docker", "build",
|
||
|
"-f", filepath.Join(b.Repo, "release/dist/qnap/Dockerfile.qpkg"),
|
||
|
"-t", "build.tailscale.io/qdk:latest",
|
||
|
filepath.Join(b.Repo, "release/dist/qnap/"),
|
||
|
)
|
||
|
out, err := cmd.CombinedOutput()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("docker build %v: %s", err, out)
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
}
|