mirror of https://github.com/knative/func.git
289 lines
9.0 KiB
Go
289 lines
9.0 KiB
Go
package client
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
// Client for a given Service Function.
|
|
type Client struct {
|
|
verbose bool // print verbose logs
|
|
name string // Service function DNS address
|
|
root string // root path of function on which to operate
|
|
domainSearchLimit int // max dirs to recurse up when deriving domain
|
|
dnsProvider DNSProvider // Provider of DNS services
|
|
initializer Initializer // Creates initial local function implementation
|
|
deployer Deployer // Deploys a Service Function
|
|
}
|
|
|
|
// DNSProvider exposes DNS services necessary for serving the Service Function.
|
|
type DNSProvider interface {
|
|
// Provide the given name by routing requests to address.
|
|
Provide(name, address string)
|
|
}
|
|
|
|
// Initializer creates the initial/stub Service Function code on first create.
|
|
type Initializer interface {
|
|
// Initialize a Service Function of the given language, in root, with name.
|
|
Initialize(name, language, path string) error
|
|
}
|
|
|
|
// Deployer of function source to running status.
|
|
type Deployer interface {
|
|
// Deploy a function of the given name whose source is located at path,
|
|
// returning an address.
|
|
Deploy(name, path string) (address string, err error)
|
|
}
|
|
|
|
// Option defines a function which when passed to the Client constructor optionally
|
|
// mutates private members at time of instantiation.
|
|
type Option func(*Client)
|
|
|
|
// WithVerbose toggles verbose logging.
|
|
func WithVerbose(v bool) Option {
|
|
return func(c *Client) {
|
|
c.verbose = v
|
|
}
|
|
}
|
|
|
|
// WithName sets the explicit DNS name for the Service Function, disabling
|
|
// name inference from path.
|
|
func WithName(dns string) Option {
|
|
return func(c *Client) {
|
|
c.name = dns
|
|
}
|
|
}
|
|
|
|
// WithRoot explicitly sets the root effective path for the client, which is used
|
|
// to write new Service Function shell files, and for determinging effective DNS
|
|
// name (unless WithName was explicitly provided). By default this is the current
|
|
// working directory of the process.
|
|
func WithRoot(path string) Option {
|
|
return func(c *Client) {
|
|
c.root = path
|
|
}
|
|
}
|
|
|
|
// WithDomainSearchLimit sets the maximum levels of upward recursion used when
|
|
// attempting to derive effective DNS name from root path. Ignored if DNS was
|
|
// explicitly set via WithName.
|
|
func WithDomainSearchLimit(limit int) Option {
|
|
return func(c *Client) {
|
|
c.domainSearchLimit = limit
|
|
}
|
|
}
|
|
|
|
// WithDNSProvider proivdes a DNS provider implementation for registering the
|
|
// effective DNS name which is either explicitly set via WithName or is derived
|
|
// from the root path.
|
|
func WithDNSProvider(provider DNSProvider) Option {
|
|
return func(c *Client) {
|
|
c.dnsProvider = provider
|
|
}
|
|
}
|
|
|
|
// WithInitializer provides the concrete implementation of the Service Function
|
|
// initializer (generates stub code on initial create).
|
|
func WithInitializer(i Initializer) Option {
|
|
return func(c *Client) {
|
|
c.initializer = i
|
|
}
|
|
}
|
|
|
|
// WithDeployer provides the concrete implementation of a deployer.
|
|
func WithDeployer(d Deployer) Option {
|
|
return func(c *Client) {
|
|
c.deployer = d
|
|
}
|
|
}
|
|
|
|
func New(options ...Option) (c *Client, err error) {
|
|
// Client with defaults overridden by optional parameters
|
|
c = &Client{
|
|
domainSearchLimit: -1, // no recursion limit deriving domain by default.
|
|
dnsProvider: &manualDNSProvider{output: os.Stdout},
|
|
initializer: &manualInitializer{output: os.Stdout},
|
|
deployer: &manualDeployer{output: os.Stdout},
|
|
}
|
|
for _, o := range options {
|
|
o(c)
|
|
}
|
|
|
|
// Convert the specified root to an absolute path.
|
|
// If no root is provided, the root is the current working directory.
|
|
c.root, err = filepath.Abs(c.root)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Derive name
|
|
// If not explicity set via the WithName option, we attempt to derive the
|
|
// name from the effective root path.
|
|
if c.name == "" {
|
|
c.name = pathToDomain(c.root, c.domainSearchLimit)
|
|
}
|
|
if c.name == "" {
|
|
return c, errors.New("Function name must be provided or be derivable from path.")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (c *Client) Create(language string) (err error) {
|
|
// Language is required
|
|
// Whether or not the given language is supported is dependant on
|
|
// the implementation of the initializer.
|
|
if language == "" {
|
|
return errors.New("language not specified")
|
|
}
|
|
|
|
// Initialize the specified root with a function template.
|
|
// TODO: detect extant and abort.
|
|
if err = c.initializer.Initialize(c.name, language, c.root); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Convert a path to a domain.
|
|
// Searches up the path string until a domain (TLD+1) is detected.
|
|
// Subdirectories are considered subdomains.
|
|
// Ex: Path: "/home/users/jane/src/example.com/admin/www"
|
|
// Returns: "www.admin.example.com"
|
|
// maxLevels is the number of directories to walk upwards beyond the current
|
|
// directory to determine domain (i.e. current directory is always considered.
|
|
// Zero indicates only consider last path element.)
|
|
func pathToDomain(path string, maxLevels int) string {
|
|
var (
|
|
// parts of the path, separated by os separator
|
|
parts = strings.Split(path, string(os.PathSeparator))
|
|
|
|
// subdomains derived from the path
|
|
subdomains []string
|
|
|
|
// domain derived from the path
|
|
domain string
|
|
)
|
|
|
|
// Loop over parts from back to front (recursing upwards), building
|
|
// optional subdomains until a root domain (TLD+1) is detected.
|
|
for i := len(parts) - 1; i >= 0; i-- {
|
|
part := parts[i]
|
|
|
|
// Support limited recursion
|
|
// Tests, for instance, need to be allowed to reliably fail by having their
|
|
// recursion contained within ./testdata if recursion is set to -1, there
|
|
// is no limit. 0 indicates only the current directory is considered.
|
|
iteration := len(parts) - 1 - i
|
|
if maxLevels >= 0 && iteration > maxLevels {
|
|
break
|
|
}
|
|
|
|
// Detect TLD+1
|
|
// If the current directory has a valid TLD plus one, it is a match.
|
|
// This is determined by using the public suffices list, which includes
|
|
// both ICANN managed TLDs as well as an extended list (matching, for
|
|
// instance 'cluster.local')
|
|
if suffix, _ := publicsuffix.EffectiveTLDPlusOne(part); suffix != "" {
|
|
domain = part
|
|
break // no directories above the nearest TLD+1 should be considered.
|
|
}
|
|
|
|
// Skip blanks
|
|
// Path elements which are blank, such as in the case of a trailing slash
|
|
// are ignored and the recursion continues, effectively collapsing ex: '//'.
|
|
if part == "" {
|
|
continue
|
|
}
|
|
|
|
// Build subdomain
|
|
// Each path element which appears before the TLD+1 is a subdomain.
|
|
// ex: '/home/users/jane/src/example.com/us-west-2/admin/www' creates the
|
|
// subdomain []string{'www', 'admin', 'us-west-2'}
|
|
subdomains = append(subdomains, part)
|
|
}
|
|
|
|
// Unable to derive domain
|
|
// If the entire path was searched, but no parts matched a TLD+1, the domain
|
|
// will be blank. In this case, the path was insufficient to derive a domain
|
|
// ex "/home/users/jane/src/test" contains no TLD, thus the final domain must
|
|
// be explicitly provided.
|
|
if domain == "" {
|
|
return ""
|
|
}
|
|
|
|
// Prepend subdomains
|
|
// If the path was a subdirectory within a TLD+1, these sudbomains
|
|
// are prepended to the TLD+1 to create the final domain.
|
|
// ex: '/home/users/jane/src/example.com/us-west-2/admin/www' yields
|
|
// www.admin.use-west-2.example.com
|
|
if len(subdomains) > 0 {
|
|
subdomains = append(subdomains, domain)
|
|
return strings.Join(subdomains, ".")
|
|
}
|
|
|
|
return domain
|
|
}
|
|
|
|
// Deploy the code at root, using the derived name, using the configured deployer.
|
|
func (c *Client) Deploy() (err error) {
|
|
// Deploy the initialized service function, returning its publicly
|
|
// addressible name for possible registration.
|
|
address, err := c.deployer.Deploy(c.name, c.root)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// TODO
|
|
// Dervive the cluster address of the service.
|
|
// Derive the public domain of the service from the directory path.
|
|
c.dnsProvider.Provide(c.name, address)
|
|
|
|
// Associate the public domain to the cluster-defined address.
|
|
return
|
|
}
|
|
|
|
// Manual implementations (noops) of required interfaces.
|
|
// In practice, the user of this client package (for example the CLI) will
|
|
// provide a concrete implementation for all of the interfaces. For testing or
|
|
// development, however, it is usefule that they are defaulted to noops and
|
|
// provded only when necessary. Unit tests for the concrete implementations
|
|
// serve to keep the core logic here separate from the imperitive.
|
|
// -----------------------------------------------------
|
|
|
|
type manualDNSProvider struct {
|
|
output io.Writer
|
|
}
|
|
|
|
func (p *manualDNSProvider) Provide(name, address string) {
|
|
if address == "" {
|
|
address = "[manually configured address]"
|
|
}
|
|
fmt.Fprintf(p.output, "Please manually configure '%v' to route requests to '%v' \n", name, address)
|
|
}
|
|
|
|
type manualInitializer struct {
|
|
output io.Writer
|
|
}
|
|
|
|
func (i *manualInitializer) Initialize(name, language, root string) error {
|
|
fmt.Fprintf(i.output, "Please create a base function for '%v' (language '%v') at path '%v'\n", name, language, root)
|
|
return nil
|
|
}
|
|
|
|
type manualDeployer struct {
|
|
output io.Writer
|
|
}
|
|
|
|
func (i *manualDeployer) Deploy(name, root string) (string, error) {
|
|
fmt.Fprintf(i.output, "Please manually deploy '%v' using code at '%v'\n", name, root)
|
|
return "", nil
|
|
}
|