There's a usecase for running workers at a different machine than the composer. For example when there's need for making images for architecture different then the composer is running at. Although osbuild has some kind of support for cross-architecture builds, we still consider it as experimental, not-yet-production-ready feature. This commit adds a support to composer and worker to communicate using TCP. To ensure safe communication through the wild worlds of Internet, TLS is not only supported but even required when using TCP. Both server and client TLS authentication are required. This means both sides must have their own private key/certificate pair and both certificates must be signed using one certificate authority. Examples how to generate all this fancy crypto stuff can be found in Makefile. Changes on the composer side: When osbuild-remote-worker.socket is started before osbuild-composer.service, osbuild-composer also serves jobqueue API on this socket. The unix domain socket is not affected by this changes - it is enabled at all times independently on the remote one. The osbuild-remote-worker.socket listens by default on TCP port 8700. When running the composer with remote worker socket enabled, the following files are required: - /etc/osbuild-composer/ca-crt.pem (CA certificate) - /etc/osbuild-composer/composer-key.pem (composer private key) - /etc/osbuild-composer/composer-crt.pem (composer certificate) Changes on the worker side: osbuild-worker has now --remote argument taking the address to a composer instance. When present, the worker will try to establish TLS secured TCP connection with the composer. When not present, the worker will use the unix domain socket method. The unit template file osbuild-remote-worker was added to simplify the spawning of workers. For example systemctl start osbuild-remote-worker@example.com starts a worker which will attempt to connect to the composer instance running on the address example.com. When running the worker with --remote argument, the following files are required: - /etc/osbuild-composer/ca-crt.pem (CA certificate) - /etc/osbuild-composer/worker-key.pem (worker private key) - /etc/osbuild-composer/worker-crt.pem (worker certificate) By default osbuild-composer.service will always spawn one local worker. If you don't want it you need to mask the default worker unit by: systemctl mask osbuild-worker@1.service Closing remarks: Remember that both composer and worker certificate must be signed by the same CA!
194 lines
4.4 KiB
Go
194 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/jobqueue"
|
|
)
|
|
|
|
const RemoteWorkerPort = 8700
|
|
|
|
type ComposerClient struct {
|
|
client *http.Client
|
|
hostname string
|
|
}
|
|
|
|
type connectionConfig struct {
|
|
CACertFile string
|
|
ClientKeyFile string
|
|
ClientCertFile string
|
|
}
|
|
|
|
func createTLSConfig(config *connectionConfig) (*tls.Config, error) {
|
|
caCertPEM, err := ioutil.ReadFile(config.CACertFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
ok := roots.AppendCertsFromPEM(caCertPEM)
|
|
if !ok {
|
|
return nil, errors.New("failed to append root certificate")
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(config.ClientCertFile, config.ClientKeyFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tls.Config{
|
|
RootCAs: roots,
|
|
Certificates: []tls.Certificate{cert},
|
|
}, nil
|
|
}
|
|
|
|
func newConnection(remoteAddress string) (net.Conn, error) {
|
|
if remoteAddress != "" {
|
|
conf, err := createTLSConfig(&connectionConfig{
|
|
CACertFile: "/etc/osbuild-composer/ca-crt.pem",
|
|
ClientKeyFile: "/etc/osbuild-composer/worker-key.pem",
|
|
ClientCertFile: "/etc/osbuild-composer/worker-crt.pem",
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
address := fmt.Sprintf("%s:%d", remoteAddress, RemoteWorkerPort)
|
|
|
|
return tls.Dial("tcp", address, conf)
|
|
}
|
|
|
|
// plain non-encrypted connection
|
|
return net.Dial("unix", "/run/osbuild-composer/job.socket")
|
|
|
|
}
|
|
|
|
func NewClient(remoteAddress string) *ComposerClient {
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(context context.Context, network, addr string) (net.Conn, error) {
|
|
return newConnection(remoteAddress)
|
|
},
|
|
},
|
|
}
|
|
hostname := remoteAddress
|
|
|
|
// in case of unix domain socket, use localhost as hostname
|
|
if hostname == "" {
|
|
hostname = "localhost"
|
|
}
|
|
|
|
return &ComposerClient{client, hostname}
|
|
}
|
|
|
|
func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
|
|
type request struct {
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
json.NewEncoder(&b).Encode(request{})
|
|
response, err := c.client.Post(c.createURL("/job-queue/v1/jobs"), "application/json", &b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != http.StatusCreated {
|
|
rawR, _ := ioutil.ReadAll(response.Body)
|
|
r := string(rawR)
|
|
return nil, errors.New(fmt.Sprintf("couldn't create job, got %d: %s", response.StatusCode, r))
|
|
}
|
|
|
|
job := &jobqueue.Job{}
|
|
err = json.NewDecoder(response.Body).Decode(job)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return job, nil
|
|
}
|
|
|
|
func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status common.ImageBuildState, result *common.ComposeResult) error {
|
|
var b bytes.Buffer
|
|
json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, result})
|
|
urlPath := fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d", job.ID.String(), job.ImageBuildID)
|
|
url := c.createURL(urlPath)
|
|
req, err := http.NewRequest("PATCH", url, &b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
response, err := c.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != http.StatusOK {
|
|
return errors.New("error setting job status")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *ComposerClient) UploadImage(job *jobqueue.Job, reader io.Reader) error {
|
|
// content type doesn't really matter
|
|
url := fmt.Sprintf("http://localhost/job-queue/v1/jobs/%s/builds/%d/image", job.ID.String(), job.ImageBuildID)
|
|
_, err := c.client.Post(url, "application/octet-stream", reader)
|
|
|
|
return err
|
|
}
|
|
|
|
func (c *ComposerClient) createURL(path string) string {
|
|
return "http://" + c.hostname + path
|
|
}
|
|
|
|
func handleJob(client *ComposerClient) error {
|
|
fmt.Println("Waiting for a new job...")
|
|
job, err := client.AddJob()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = client.UpdateJob(job, common.IBRunning, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Running job %s\n", job.ID.String())
|
|
result, err := job.Run(client)
|
|
if err != nil {
|
|
log.Printf(" Job failed: %v", err)
|
|
return client.UpdateJob(job, common.IBFailed, result)
|
|
}
|
|
|
|
return client.UpdateJob(job, common.IBFinished, result)
|
|
}
|
|
|
|
func main() {
|
|
var remoteAddress string
|
|
flag.StringVar(&remoteAddress, "remote", "", "Connect to a remote composer using the specified address")
|
|
flag.Parse()
|
|
|
|
client := NewClient(remoteAddress)
|
|
for {
|
|
if err := handleJob(client); err != nil {
|
|
log.Fatalf("Failed to handle job: " + err.Error())
|
|
}
|
|
}
|
|
}
|