blob: 0ec8d97b36d57098032385e785888553c1e77aa3 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package portdiscovery hosts common code shared by CFT containerized services.
// It provides APIs to write service port number to a metadata file within the
// container that allows clients (e.g. cros-tool-runner) to look up the info.
// More info at go/cft-port-discovery
package portdiscovery
import (
"errors"
"fmt"
"log"
"os"
"regexp"
"time"
)
const (
// Definitions of keys in the metadata file
servicePort = "SERVICE_PORT"
serviceName = "SERVICE_NAME"
serviceVersion = "SERVICE_VERSION"
startTime = "# SERVICE_START_TIME" // Debug only, appears as comments.
// File name of the metadata file
metaFileName = ".cftmeta"
)
// Metadata represents the data to be written into the metadata file. The data
// will be translated into standardized key value pair and written into the file
// following the format of an env file. All values are in string format and will
// be wrapped with single quotes during the conversion.
// The metadata file is kept at a minimum level for port discovery only as more
// complicated and structured data could/should be surfaced through service end
// points.
// Future extensions may consider adding a Config object to control the behavior
// and a map of free formed key-value pair per container (not recommended unless
// absolutely necessary).
type Metadata struct {
Port string // Mandatory
Name string // Optional
Version string // Optional
}
// Api is the interface of the portdiscovery utility.
type Api interface {
// WriteMetadata writes metadata to a file based on Metadata.
WriteMetadata(Metadata) error
// GetPortFromAddress returns the port number of the given the address. The
// address should be in the format of ".*:port".
GetPortFromAddress(string) (string, error)
}
// PortDiscovery is an implementation of Api.
type PortDiscovery struct {
Api
Logger *log.Logger
}
func (p *PortDiscovery) WriteMetadata(metadata Metadata) error {
p.getLogger().Printf("info: received metadata %v", metadata)
if metadata.Port == "" {
return errors.New("invalid metadata: Port is mandatory")
}
metaFile := p.getMetaFilePath()
f, err := os.OpenFile(metaFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = p.writeLine(f, servicePort, metadata.Port)
if err != nil {
return err
}
_, err = p.writeLine(f, serviceName, metadata.Name)
if err != nil {
return err
}
_, err = p.writeLine(f, serviceVersion, metadata.Version)
if err != nil {
return err
}
_, err = p.writeLine(f, startTime, time.Now().Format(time.RFC3339))
if err != nil {
return err
}
p.getLogger().Printf("info: persisted metadata to %s", metaFile)
return nil
}
func (p *PortDiscovery) GetPortFromAddress(address string) (string, error) {
r := regexp.MustCompile(`.*:(\d+)$`)
match := r.FindStringSubmatch(address)
if match == nil {
p.getLogger().Printf("error: cannot apply pattern to address: %s", address)
return "", errors.New(fmt.Sprintf("unable to get port from %s", address))
}
return match[1], nil
}
func (p *PortDiscovery) getLogger() *log.Logger {
if p.Logger == nil {
p.Logger = log.Default()
}
return p.Logger
}
// writeLine writes a single line to file when value is not empty.
// The format follows the syntax of environment variable declaration:
// KEY=value
// Note that value will always be single quoted.
func (p *PortDiscovery) writeLine(file *os.File, key string, value string) (int, error) {
if value == "" {
return 0, nil
}
return file.WriteString(fmt.Sprintf("%s='%s'\n", key, value))
}
// getHome returns the home directory path.
func (p *PortDiscovery) getHome() string {
dirname, err := os.UserHomeDir()
if err != nil {
p.getLogger().Printf("warning: error when getting home dir: %s", err)
return ""
}
return dirname
}
// getMetaFilePath returns the full path of metadata file.
func (p *PortDiscovery) getMetaFilePath() string {
return fmt.Sprintf("%s/%s", p.getHome(), metaFileName)
}
// WriteServiceMetadata simplifies writing service metadata for all cft services. The
// service address should be in the format of ".*:port".
func WriteServiceMetadata(name string, serviceAddress string, logger *log.Logger) error {
// Write port number to ~/.cftmeta for go/cft-port-discovery
pdUtil := PortDiscovery{Logger: logger}
servicePort, _ := pdUtil.GetPortFromAddress(serviceAddress)
serviceMetadata := Metadata{
Port: servicePort,
Name: name,
}
err := pdUtil.WriteMetadata(serviceMetadata)
return err
}