mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
226 lines
5.3 KiB
Go
226 lines
5.3 KiB
Go
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License-AGPL.txt in the project root for license information.
|
|
|
|
// +build linux
|
|
|
|
package hosts
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// Host maps an IP address to a hostname
|
|
type Host struct {
|
|
Addr string `json:"addr"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// HostSource provides a hostname and its corresponding IP address
|
|
type HostSource interface {
|
|
Name() string
|
|
|
|
// Start starts the source
|
|
Start() error
|
|
|
|
// Source provides hosts on the channel
|
|
Source() <-chan []Host
|
|
|
|
// Stop stops this source from providing hosts
|
|
Stop()
|
|
}
|
|
|
|
const (
|
|
// wsmanNodeMarkerComment is added to the end of a line to mark it as
|
|
// added by ws-daemon.
|
|
fmtMarkerComment = " # added by ws-daemon %s: %s"
|
|
)
|
|
|
|
// Controller controls a hosts resolvable domains
|
|
type Controller interface {
|
|
io.Closer
|
|
Start()
|
|
|
|
DidUpdate() bool
|
|
}
|
|
|
|
// NewDirectController creates a new hosts file controller
|
|
func NewDirectController(name, hostsFile string, sources ...HostSource) (*DirectController, error) {
|
|
lockFD, err := syscall.Open(hostsFile, syscall.O_RDWR, 0644)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot open hosts file: %w", err)
|
|
}
|
|
fd := os.NewFile(uintptr(lockFD), hostsFile)
|
|
|
|
return &DirectController{
|
|
Name: name,
|
|
Sources: sources,
|
|
hostsFD: fd,
|
|
lockFD: lockFD,
|
|
stop: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// DirectController regularly updates the host's /etc/hosts file to add hostnames
|
|
// which can be resolved by the kubelet. We use this to resolve the registry.
|
|
type DirectController struct {
|
|
Name string
|
|
Sources []HostSource
|
|
|
|
hostsFD *os.File
|
|
lockFD int
|
|
stop chan struct{}
|
|
didUpdate bool
|
|
}
|
|
|
|
type hostUpdate struct {
|
|
Src string
|
|
Hosts []Host
|
|
}
|
|
|
|
// Start runs the hosts controller - this function does not return until the controller
|
|
// is stopped. It's intended to be called as a Go routine.
|
|
func (g *DirectController) Start() {
|
|
updates := make(chan hostUpdate)
|
|
go g.updateHostsFile(updates)
|
|
|
|
for _, src := range g.Sources {
|
|
go func(src HostSource) {
|
|
defer src.Stop()
|
|
defer log.WithField("name", src.Name()).Info("hosts source shutting down")
|
|
|
|
for {
|
|
select {
|
|
case inc := <-src.Source():
|
|
if inc == nil {
|
|
return
|
|
}
|
|
|
|
updates <- hostUpdate{src.Name(), inc}
|
|
case <-g.stop:
|
|
return
|
|
}
|
|
}
|
|
}(src)
|
|
|
|
err := src.Start()
|
|
if err != nil {
|
|
log.WithField("name", src.Name()).WithError(err).Error("cannot start host source")
|
|
} else {
|
|
log.WithField("name", src.Name()).Info("start hosts source")
|
|
}
|
|
}
|
|
}
|
|
|
|
// DidUpdate returns true if the host controller wrote its first update
|
|
func (g *DirectController) DidUpdate() bool {
|
|
return g.didUpdate
|
|
}
|
|
|
|
// Close stops this controller
|
|
func (g *DirectController) Close() error {
|
|
close(g.stop)
|
|
return nil
|
|
}
|
|
|
|
func (g *DirectController) updateHostsFile(inc <-chan hostUpdate) {
|
|
for {
|
|
var update hostUpdate
|
|
select {
|
|
case <-g.stop:
|
|
defer log.Info("hosts updater shutting down")
|
|
return
|
|
case update = <-inc:
|
|
}
|
|
|
|
err := func() (err error) {
|
|
ok, err := g.lockHostsFile()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !ok {
|
|
return xerrors.Errorf("cannot acquire lock")
|
|
}
|
|
defer g.unlockHostsFile()
|
|
|
|
_, err = g.hostsFD.Seek(0, 0)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot jump to start of hosts file: %w", err)
|
|
}
|
|
fc, err := ioutil.ReadAll(g.hostsFD)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot read hosts file: %w", err)
|
|
}
|
|
|
|
wsmanNodeMarkerComment := fmt.Sprintf(fmtMarkerComment, g.Name, update.Src)
|
|
|
|
var newhosts []string
|
|
// add all former hosts entries that did not come from us
|
|
lines := strings.Split(string(fc), "\n")
|
|
for _, l := range lines {
|
|
if strings.Contains(l, wsmanNodeMarkerComment) {
|
|
continue
|
|
}
|
|
if l == "" {
|
|
continue
|
|
}
|
|
|
|
newhosts = append(newhosts, strings.TrimSpace(l))
|
|
}
|
|
// add all updated hosts
|
|
for _, h := range update.Hosts {
|
|
l := fmt.Sprintf("%s\t%s\t%s", h.Addr, h.Name, wsmanNodeMarkerComment)
|
|
newhosts = append(newhosts, l)
|
|
}
|
|
|
|
newhostsfc := strings.Join(append(newhosts, ""), "\n")
|
|
|
|
// write back
|
|
_, err = g.hostsFD.Seek(0, 0)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot jump to start of hosts file: %w", err)
|
|
}
|
|
err = g.hostsFD.Truncate(0)
|
|
if err != nil {
|
|
log.WithError(err).Warn("cannot truncate hosts file - this might result in broken host resolution on the node")
|
|
}
|
|
_, err = g.hostsFD.WriteString(newhostsfc)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot write hosts file after truncating it - this will break hosts resolution on the node: %w", err)
|
|
}
|
|
g.didUpdate = true
|
|
log.WithField("hosts", newhostsfc).Debug("updated hosts file")
|
|
return
|
|
}()
|
|
|
|
if err != nil {
|
|
log.WithError(err).WithField("source", update.Src).Error("hosts update failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *DirectController) lockHostsFile() (lockAcquired bool, err error) {
|
|
err = syscall.Flock(g.lockFD, syscall.LOCK_EX|syscall.LOCK_NB)
|
|
if err == syscall.EWOULDBLOCK {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, xerrors.Errorf("cannot acqiure lock: %w", err)
|
|
}
|
|
|
|
lockAcquired = true
|
|
return
|
|
}
|
|
|
|
func (g *DirectController) unlockHostsFile() (err error) {
|
|
return syscall.Flock(g.lockFD, syscall.LOCK_UN)
|
|
}
|