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 }