mirror of https://github.com/knative/func.git
				
				
				
			
		
			
				
	
	
		
			973 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			973 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
| package function
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"runtime/debug"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/mitchellh/go-homedir"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// DefaultRegistry through which containers of Functions will be shuttled.
 | |
| 	DefaultRegistry = "docker.io"
 | |
| 
 | |
| 	// DefaultTemplate is the default Function signature / environmental context
 | |
| 	// of the resultant function.  All runtimes are expected to have at least
 | |
| 	// one implementation of each supported function signature.  Currently that
 | |
| 	// includes an HTTP Handler ("http") and Cloud Events handler ("events")
 | |
| 	DefaultTemplate = "http"
 | |
| 
 | |
| 	// DefaultVersion is the initial value for string members whose implicit type
 | |
| 	// is a semver.
 | |
| 	DefaultVersion = "0.0.0"
 | |
| 
 | |
| 	// DefaultConfigPath is used in the unlikely event that
 | |
| 	// the user has no home directory (no ~), there is no
 | |
| 	// XDG_CONFIG_HOME set, and no WithConfigPath was used.
 | |
| 	DefaultConfigPath = ".config/func"
 | |
| 
 | |
| 	// DefaultBuildType is the default build type for a Function
 | |
| 	DefaultBuildType = BuildTypeLocal
 | |
| )
 | |
| 
 | |
| // Client for managing Function instances.
 | |
| type Client struct {
 | |
| 	repositoriesPath  string            // path to repositories
 | |
| 	repositoriesURI   string            // repo URI (overrides repositories path)
 | |
| 	verbose           bool              // print verbose logs
 | |
| 	builder           Builder           // Builds a runnable image source
 | |
| 	pusher            Pusher            // Pushes Funcation image to a remote
 | |
| 	deployer          Deployer          // Deploys or Updates a Function
 | |
| 	runner            Runner            // Runs the Function locally
 | |
| 	remover           Remover           // Removes remote services
 | |
| 	lister            Lister            // Lists remote services
 | |
| 	describer         Describer         // Describes Function instances
 | |
| 	dnsProvider       DNSProvider       // Provider of DNS services
 | |
| 	registry          string            // default registry for OCI image tags
 | |
| 	progressListener  ProgressListener  // progress listener
 | |
| 	repositories      *Repositories     // Repositories management
 | |
| 	templates         *Templates        // Templates management
 | |
| 	instances         *Instances        // Function Instances management
 | |
| 	transport         http.RoundTripper // Customizable internal transport
 | |
| 	pipelinesProvider PipelinesProvider // CI/CD pipelines management
 | |
| }
 | |
| 
 | |
| // ErrNotBuilt indicates the Function has not yet been built.
 | |
| var ErrNotBuilt = errors.New("not built")
 | |
| 
 | |
| // Builder of Function source to runnable image.
 | |
| type Builder interface {
 | |
| 	// Build a Function project with source located at path.
 | |
| 	Build(context.Context, Function) error
 | |
| }
 | |
| 
 | |
| // Pusher of Function image to a registry.
 | |
| type Pusher interface {
 | |
| 	// Push the image of the Function.
 | |
| 	// Returns Image Digest - SHA256 hash of the produced image
 | |
| 	Push(ctx context.Context, f Function) (string, error)
 | |
| }
 | |
| 
 | |
| // Deployer of Function source to running status.
 | |
| type Deployer interface {
 | |
| 	// Deploy a Function of given name, using given backing image.
 | |
| 	Deploy(context.Context, Function) (DeploymentResult, error)
 | |
| }
 | |
| 
 | |
| type DeploymentResult struct {
 | |
| 	Status Status
 | |
| 	URL    string
 | |
| }
 | |
| 
 | |
| // Status of the Function from the DeploymentResult
 | |
| type Status int
 | |
| 
 | |
| const (
 | |
| 	Failed Status = iota
 | |
| 	Deployed
 | |
| 	Updated
 | |
| )
 | |
| 
 | |
| // Runner runs the Function locally.
 | |
| type Runner interface {
 | |
| 	// Run the Function, returning a Job with metadata, error channels, and
 | |
| 	// a stop function.The process can be stopped by running the returned stop
 | |
| 	// function, either on context cancellation or in a defer.
 | |
| 	Run(context.Context, Function) (*Job, error)
 | |
| }
 | |
| 
 | |
| // Remover of deployed services.
 | |
| type Remover interface {
 | |
| 	// Remove the Function from remote.
 | |
| 	Remove(ctx context.Context, name string) error
 | |
| }
 | |
| 
 | |
| // Lister of deployed functions.
 | |
| type Lister interface {
 | |
| 	// List the Functions currently deployed.
 | |
| 	List(ctx context.Context) ([]ListItem, error)
 | |
| }
 | |
| 
 | |
| type ListItem struct {
 | |
| 	Name      string `json:"name" yaml:"name"`
 | |
| 	Namespace string `json:"namespace" yaml:"namespace"`
 | |
| 	Runtime   string `json:"runtime" yaml:"runtime"`
 | |
| 	URL       string `json:"url" yaml:"url"`
 | |
| 	Ready     string `json:"ready" yaml:"ready"`
 | |
| }
 | |
| 
 | |
| // ProgressListener is notified of task progress.
 | |
| type ProgressListener interface {
 | |
| 	// SetTotal steps of the given task.
 | |
| 	SetTotal(int)
 | |
| 	// Increment to the next step with the given message.
 | |
| 	Increment(message string)
 | |
| 	// Complete signals completion, which is expected to be somewhat different
 | |
| 	// than a step increment.
 | |
| 	Complete(message string)
 | |
| 	// Stopping indicates the process is in the state of stopping, such as when a
 | |
| 	// context cancelation has been received
 | |
| 	Stopping()
 | |
| 	// Done signals a cessation of progress updates.  Should be called in a defer
 | |
| 	// statement to ensure the progress listener can stop any outstanding tasks
 | |
| 	// such as synchronous user updates.
 | |
| 	Done()
 | |
| }
 | |
| 
 | |
| // Describer of Function instances
 | |
| type Describer interface {
 | |
| 	// Describe the named Function in the remote environment.
 | |
| 	Describe(ctx context.Context, name string) (Instance, error)
 | |
| }
 | |
| 
 | |
| // Instance data about the runtime state of a Function in a given environment.
 | |
| //
 | |
| // A Function instance is a logical running Function space, which share
 | |
| // a unique route (or set of routes).  Due to autoscaling and load balancing,
 | |
| // there is a one to many relationship between a given route and processes.
 | |
| // By default the system creates the 'local' and 'remote' named instances
 | |
| // when a Function is run (locally) and deployed, respectively.
 | |
| // See the .Instances(f) accessor for the map of named environments to these
 | |
| // Function Information structures.
 | |
| type Instance struct {
 | |
| 	// Route is the primary route of a Function instance.
 | |
| 	Route string
 | |
| 	// Routes is the primary route plus any other route at which the Function
 | |
| 	// can be contacted.
 | |
| 	Routes        []string       `json:"routes" yaml:"routes"`
 | |
| 	Name          string         `json:"name" yaml:"name"`
 | |
| 	Image         string         `json:"image" yaml:"image"`
 | |
| 	Namespace     string         `json:"namespace" yaml:"namespace"`
 | |
| 	Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
 | |
| }
 | |
| 
 | |
| // Subscriptions currently active to event sources
 | |
| type Subscription struct {
 | |
| 	Source string `json:"source" yaml:"source"`
 | |
| 	Type   string `json:"type" yaml:"type"`
 | |
| 	Broker string `json:"broker" yaml:"broker"`
 | |
| }
 | |
| 
 | |
| // DNSProvider exposes DNS services necessary for serving the Function.
 | |
| type DNSProvider interface {
 | |
| 	// Provide the given name by routing requests to address.
 | |
| 	Provide(Function) error
 | |
| }
 | |
| 
 | |
| // PipelinesProvider manages lifecyle of CI/CD pipelines used by a Function
 | |
| type PipelinesProvider interface {
 | |
| 	Run(context.Context, Function) error
 | |
| 	Remove(context.Context, Function) error
 | |
| }
 | |
| 
 | |
| // New client for Function management.
 | |
| func New(options ...Option) *Client {
 | |
| 	// Instantiate client with static defaults.
 | |
| 	c := &Client{
 | |
| 		builder:           &noopBuilder{output: os.Stdout},
 | |
| 		pusher:            &noopPusher{output: os.Stdout},
 | |
| 		deployer:          &noopDeployer{output: os.Stdout},
 | |
| 		runner:            &noopRunner{output: os.Stdout},
 | |
| 		remover:           &noopRemover{output: os.Stdout},
 | |
| 		lister:            &noopLister{output: os.Stdout},
 | |
| 		describer:         &noopDescriber{output: os.Stdout},
 | |
| 		dnsProvider:       &noopDNSProvider{output: os.Stdout},
 | |
| 		progressListener:  &NoopProgressListener{},
 | |
| 		pipelinesProvider: &noopPipelinesProvider{},
 | |
| 		repositoriesPath:  filepath.Join(ConfigPath(), "repositories"),
 | |
| 		transport:         http.DefaultTransport,
 | |
| 	}
 | |
| 	for _, o := range options {
 | |
| 		o(c)
 | |
| 	}
 | |
| 
 | |
| 	// Initialize sub-managers using now-fully-initialized client.
 | |
| 	c.repositories = newRepositories(c)
 | |
| 	c.templates = newTemplates(c)
 | |
| 	c.instances = newInstances(c)
 | |
| 
 | |
| 	// Trigger the creation of the config and repository paths
 | |
| 	_ = ConfigPath()         // Config is package-global scoped
 | |
| 	_ = c.RepositoriesPath() // Repositories is Client-specific
 | |
| 
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| // The default config path is evaluated in the following order, from lowest
 | |
| // to highest precedence.
 | |
| // 1.  The static default is DefaultConfigPath (./.config/func)
 | |
| // 2.  ~/.config/func if it exists (can be expanded: user has a home dir)
 | |
| // 3.  The value of $XDG_CONFIG_PATH/func if the environment variable exists.
 | |
| // The path will be created if it does not already exist.
 | |
| func ConfigPath() (path string) {
 | |
| 	path = DefaultConfigPath
 | |
| 
 | |
| 	// ~/.config/func is the default if ~ can be expanded
 | |
| 	if home, err := homedir.Expand("~"); err == nil {
 | |
| 		path = filepath.Join(home, ".config", "func")
 | |
| 	}
 | |
| 
 | |
| 	// 'XDG_CONFIG_HOME/func' takes precidence if defined
 | |
| 	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
 | |
| 		path = filepath.Join(xdg, "func")
 | |
| 	}
 | |
| 
 | |
| 	mkdir(path) // make sure it exists
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // RepositoriesPath accesses the currently effective repositories path,
 | |
| // which defaults to [ConfigPath]/repositories but can be set explicitly using
 | |
| // the WithRepositoriesPath option when creating the client..
 | |
| // The path will be created if it does not already exist.
 | |
| func (c *Client) RepositoriesPath() (path string) {
 | |
| 	path = c.repositories.Path()
 | |
| 	mkdir(path) // make sure it exists
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // RepositoriesPath is a convenience method for accessing the default path to
 | |
| // repositories that will be used by new instances of a Client unless options
 | |
| // such as WithRepositoriesPath are used to override.
 | |
| // The path will be created if it does not already exist.
 | |
| func RepositoriesPath() string {
 | |
| 	return New().RepositoriesPath()
 | |
| }
 | |
| 
 | |
| // OPTIONS
 | |
| // ---------
 | |
| 
 | |
| // 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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithBuilder provides the concrete implementation of a builder.
 | |
| func WithBuilder(d Builder) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.builder = d
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithPusher provides the concrete implementation of a pusher.
 | |
| func WithPusher(d Pusher) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.pusher = d
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithDeployer provides the concrete implementation of a deployer.
 | |
| func WithDeployer(d Deployer) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.deployer = d
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithRunner provides the concrete implementation of a deployer.
 | |
| func WithRunner(r Runner) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.runner = r
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithRemover provides the concrete implementation of a remover.
 | |
| func WithRemover(r Remover) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.remover = r
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithLister provides the concrete implementation of a lister.
 | |
| func WithLister(l Lister) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.lister = l
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithDescriber provides a concrete implementation of a Function describer.
 | |
| func WithDescriber(describer Describer) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.describer = describer
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithProgressListener provides a concrete implementation of a listener to
 | |
| // be notified of progress updates.
 | |
| func WithProgressListener(p ProgressListener) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.progressListener = p
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithRepositoriesPath sets the location on disk to use for extensible template
 | |
| // repositories.  Extensible template repositories are additional templates
 | |
| // that exist on disk and are not built into the binary.
 | |
| func WithRepositoriesPath(path string) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.repositoriesPath = path
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithRepository sets a specific URL to a Git repository from which to pull
 | |
| // templates.  This setting's existence precldes the use of either the inbuilt
 | |
| // templates or any repositories from the extensible repositories path.
 | |
| func WithRepository(uri string) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.repositoriesURI = uri
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithRegistry sets the default registry which is consulted when an image name/tag
 | |
| // is not explocitly provided.  Can be fully qualified, including the registry
 | |
| // (ex: 'quay.io/myname') or simply the namespace 'myname' which indicates the
 | |
| // the use of the default registry.
 | |
| func WithRegistry(registry string) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.registry = registry
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithTransport sets a custom transport to use internally.
 | |
| func WithTransport(t http.RoundTripper) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.transport = t
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithPipelinesProvider sets implementation of provider responsible for CI/CD pipelines
 | |
| func WithPipelinesProvider(pp PipelinesProvider) Option {
 | |
| 	return func(c *Client) {
 | |
| 		c.pipelinesProvider = pp
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ACCESSORS
 | |
| // ---------
 | |
| 
 | |
| // Repositories accessor
 | |
| func (c *Client) Repositories() *Repositories {
 | |
| 	return c.repositories
 | |
| }
 | |
| 
 | |
| // Templates accessor
 | |
| func (c *Client) Templates() *Templates {
 | |
| 	return c.templates
 | |
| }
 | |
| 
 | |
| // Instances accessor
 | |
| func (c *Client) Instances() *Instances {
 | |
| 	return c.instances
 | |
| }
 | |
| 
 | |
| // Runtimes available in totality.
 | |
| // Not all repository/template combinations necessarily exist,
 | |
| // and further validation is performed when a template+runtime is chosen.
 | |
| // from a given repository.  This is the global list of all available.
 | |
| // Returned list is unique and sorted.
 | |
| func (c *Client) Runtimes() ([]string, error) {
 | |
| 	runtimes := newSortedSet()
 | |
| 
 | |
| 	// Gather all runtimes from all repositories
 | |
| 	// into a uniqueness map
 | |
| 	repositories, err := c.Repositories().All()
 | |
| 	if err != nil {
 | |
| 		return []string{}, err
 | |
| 	}
 | |
| 	for _, repo := range repositories {
 | |
| 		for _, runtime := range repo.Runtimes {
 | |
| 			runtimes.Add(runtime.Name)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Return a unique, sorted list of runtimes
 | |
| 	return runtimes.Items(), nil
 | |
| }
 | |
| 
 | |
| // LIFECYCLE METHODS
 | |
| // -----------------
 | |
| 
 | |
| // New Function.
 | |
| // Use Create, Build and Deploy independently for lower level control.
 | |
| func (c *Client) New(ctx context.Context, cfg Function) (err error) {
 | |
| 	c.progressListener.SetTotal(3)
 | |
| 	// Always start a concurrent routine listening for context cancellation.
 | |
| 	// On this event, immediately indicate the task is canceling.
 | |
| 	// (this is useful, for example, when a progress listener is mutating
 | |
| 	// stdout, and a context cancelation needs to free up stdout entirely for
 | |
| 	// the status or error from said cancelltion.
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 
 | |
| 	// Create Function at path indidcated by Config
 | |
| 	if err = c.Create(cfg); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Load the now-initialized Function.
 | |
| 	f, err := NewFunction(cfg.Root)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Build the now-initialized Function
 | |
| 	c.progressListener.Increment("Building container image")
 | |
| 	if err = c.Build(ctx, f.Root); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Push the produced function image
 | |
| 	c.progressListener.Increment("Pushing container image to registry")
 | |
| 	if err = c.Push(ctx, f.Root); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Deploy the initialized Function, returning its publicly
 | |
| 	// addressible name for possible registration.
 | |
| 	c.progressListener.Increment("Deploying Function to cluster")
 | |
| 	if err = c.Deploy(ctx, f.Root); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Create an external route to the Function
 | |
| 	c.progressListener.Increment("Creating route to Function")
 | |
| 	if err = c.Route(f.Root); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.progressListener.Complete("Done")
 | |
| 
 | |
| 	// TODO: use the knative client during deployment such that the actual final
 | |
| 	// route can be returned from the deployment step, passed to the DNS Router
 | |
| 	// for routing actual traffic, and returned here.
 | |
| 	if c.verbose {
 | |
| 		fmt.Printf("https://%v/\n", f.Name)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Create a new Function from the given defaults.
 | |
| // <path> will default to the absolute path of the current working directory.
 | |
| // <name> will default to the current working directory.
 | |
| // When <name> is provided but <path> is not, a directory <name> is created
 | |
| // in the current working directory and used for <path>.
 | |
| func (c *Client) Create(cfg Function) (err error) {
 | |
| 	// convert Root path to absolute
 | |
| 	cfg.Root, err = filepath.Abs(cfg.Root)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Create project root directory, if it doesn't already exist
 | |
| 	if err = os.MkdirAll(cfg.Root, 0755); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Create should never clobber a pre-existing Function
 | |
| 	hasFunc, err := hasInitializedFunction(cfg.Root)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if hasFunc {
 | |
| 		return fmt.Errorf("Function at '%v' already initialized", cfg.Root)
 | |
| 	}
 | |
| 
 | |
| 	// Path is defaulted to the current working directory
 | |
| 	if cfg.Root == "" {
 | |
| 		if cfg.Root, err = os.Getwd(); err != nil {
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Name is defaulted to the directory of the given path.
 | |
| 	if cfg.Name == "" {
 | |
| 		cfg.Name = nameFromPath(cfg.Root)
 | |
| 	}
 | |
| 
 | |
| 	// The path for the new Function should not have any contentious files
 | |
| 	// (hidden files OK, unless it's one used by Func)
 | |
| 	if err := assertEmptyRoot(cfg.Root); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Create a new Function (in memory)
 | |
| 	f := NewFunctionWith(cfg)
 | |
| 
 | |
| 	// Create a .func diretory which is also added to a .gitignore
 | |
| 	if err = createRuntimeDir(f); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Write out the new Function's Template files.
 | |
| 	// Templates contain values which may result in the Function being mutated
 | |
| 	// (default builders, etc), so a new (potentially mutated) Function is
 | |
| 	// returned from Templates.Write
 | |
| 	f, err = c.Templates().Write(f)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Mark the Function as having been created
 | |
| 	f.Created = time.Now()
 | |
| 	if err = f.Write(); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// TODO: Create a status structure and return it for clients to use
 | |
| 	// for output, such as from the CLI.
 | |
| 	if c.verbose {
 | |
| 		fmt.Printf("Builder:       %s\n", f.Builder)
 | |
| 		if len(f.Buildpacks) > 0 {
 | |
| 			fmt.Println("Buildpacks:")
 | |
| 			for _, b := range f.Buildpacks {
 | |
| 				fmt.Printf("           ... %s\n", b)
 | |
| 			}
 | |
| 		}
 | |
| 		fmt.Println("Function project created")
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // createRuntimeDir creates a .func directory in the root of the given
 | |
| // Function which is also registered as ignored in .gitignore
 | |
| // TODO: Mutate extant .gitignore file if it exists rather than failing
 | |
| // if present (see contentious files in function.go), such that a user
 | |
| // can `git init` a directory prior to `func init` in the same directory).
 | |
| func createRuntimeDir(f Function) error {
 | |
| 	if err := os.MkdirAll(filepath.Join(f.Root, RunDataDir), os.ModePerm); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	gitignore := `
 | |
| # Functions use the .func directory for local runtime data which should
 | |
| # generally not be tracked in source control:
 | |
| /.func
 | |
| `
 | |
| 	return os.WriteFile(filepath.Join(f.Root, ".gitignore"), []byte(gitignore), os.ModePerm)
 | |
| 
 | |
| }
 | |
| 
 | |
| // Build the Function at path. Errors if the Function is either unloadable or does
 | |
| // not contain a populated Image.
 | |
| func (c *Client) Build(ctx context.Context, path string) (err error) {
 | |
| 	c.progressListener.Increment("Building function image")
 | |
| 
 | |
| 	// If not logging verbosely, the ongoing progress of the build will not
 | |
| 	// be streaming to stdout, and the lack of activity has been seen to cause
 | |
| 	// users to prematurely exit due to the sluggishness of pulling large images
 | |
| 	if !c.verbose {
 | |
| 		c.printBuildActivity(ctx) // print friendly messages until context is canceled
 | |
| 	}
 | |
| 
 | |
| 	f, err := NewFunction(path)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Derive Image from the path (precedence is given to extant config)
 | |
| 	if f.Image, err = DerivedImage(path, c.registry); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err = c.builder.Build(ctx, f); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Write (save) - Serialize the Function to disk
 | |
| 	// Will now contain populated image tag.
 | |
| 	if err = f.Write(); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// TODO: create a status structure and return it here for optional
 | |
| 	// use by the cli for user echo (rather than rely on verbose mode here)
 | |
| 	message := fmt.Sprintf("🙌 Function image built: %v", f.Image)
 | |
| 	if runtime.GOOS == "windows" {
 | |
| 		message = fmt.Sprintf("Function image built: %v", f.Image)
 | |
| 	}
 | |
| 	c.progressListener.Increment(message)
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func (c *Client) printBuildActivity(ctx context.Context) {
 | |
| 	m := []string{
 | |
| 		"Still building",
 | |
| 		"Still building",
 | |
| 		"Yes, still building",
 | |
| 		"Don't give up on me",
 | |
| 		"Still building",
 | |
| 		"This is taking a while",
 | |
| 	}
 | |
| 	i := 0
 | |
| 	ticker := time.NewTicker(10 * time.Second)
 | |
| 	go func() {
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-ticker.C:
 | |
| 				c.progressListener.Increment(m[i])
 | |
| 				i++
 | |
| 				i = i % len(m)
 | |
| 			case <-ctx.Done():
 | |
| 				c.progressListener.Stopping()
 | |
| 				ticker.Stop()
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| }
 | |
| 
 | |
| // Deploy the Function at path. Errors if the Function has not been
 | |
| // initialized with an image tag.
 | |
| func (c *Client) Deploy(ctx context.Context, path string) (err error) {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 
 | |
| 	f, err := NewFunction(path)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Functions must be built (have an associated image) before being deployed.
 | |
| 	// Note that externally built images may be specified in the func.yaml
 | |
| 	if !f.Built() {
 | |
| 		return ErrNotBuilt
 | |
| 	}
 | |
| 
 | |
| 	// Deploy a new or Update the previously-deployed Function
 | |
| 	c.progressListener.Increment("Deploying function to the cluster")
 | |
| 	result, err := c.deployer.Deploy(ctx, f)
 | |
| 	if result.Status == Deployed {
 | |
| 		c.progressListener.Increment(fmt.Sprintf("Function deployed at URL: %v", result.URL))
 | |
| 	} else if result.Status == Updated {
 | |
| 		c.progressListener.Increment(fmt.Sprintf("Function updated at URL: %v", result.URL))
 | |
| 	}
 | |
| 
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // RunPipeline runs a Pipeline to Build and deploy the Function at path.
 | |
| func (c *Client) RunPipeline(ctx context.Context, path string, git Git) (err error) {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 
 | |
| 	f, err := NewFunction(path)
 | |
| 	if err != nil {
 | |
| 		err = fmt.Errorf("failed to laod function: %w", err)
 | |
| 		return
 | |
| 	}
 | |
| 	f.Git = git
 | |
| 
 | |
| 	// Build and deploy function using Pipeline
 | |
| 	err = c.pipelinesProvider.Run(ctx, f)
 | |
| 	if err != nil {
 | |
| 		err = fmt.Errorf("failed to run pipeline: %w", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (c *Client) Route(path string) (err error) {
 | |
| 	// Ensure that the allocated final address is enabled with the
 | |
| 	// configured DNS provider.
 | |
| 	// NOTE:
 | |
| 	// DNS and TLS are provisioned by Knative Serving + cert-manager,
 | |
| 	// but DNS subdomain CNAME to the Kourier Load Balancer is
 | |
| 	// still manual, and the initial cluster config to suppot the TLD
 | |
| 	// is still manual.
 | |
| 	f, err := NewFunction(path)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	return c.dnsProvider.Provide(f)
 | |
| }
 | |
| 
 | |
| // Run the Function whose code resides at root.
 | |
| // On start, the chosen port is sent to the provided started channel
 | |
| func (c *Client) Run(ctx context.Context, root string) (job *Job, err error) {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 
 | |
| 	// Load the Function
 | |
| 	f, err := NewFunction(root)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if !f.Initialized() {
 | |
| 		// TODO: this needs a test.
 | |
| 		err = fmt.Errorf("the given path '%v' does not contain an initialized "+
 | |
| 			"Function.  Please create one at this path in order to run", root)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Run the Function, which returns a Job for use interacting (at arms length)
 | |
| 	// with that running task (which is likely inside a container process).
 | |
| 	if job, err = c.runner.Run(ctx, f); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Return to the caller the effective port, a function to call to trigger
 | |
| 	// stop, and a channel on which can be received runtime errors.
 | |
| 	return job, nil
 | |
| }
 | |
| 
 | |
| // Info for a Function.  Name takes precidence.  If no name is provided,
 | |
| // the Function defined at root is used.
 | |
| func (c *Client) Info(ctx context.Context, name, root string) (d Instance, err error) {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 	// If name is provided, it takes precidence.
 | |
| 	// Otherwise load the Function defined at root.
 | |
| 	if name != "" {
 | |
| 		return c.describer.Describe(ctx, name)
 | |
| 	}
 | |
| 
 | |
| 	f, err := NewFunction(root)
 | |
| 	if err != nil {
 | |
| 		return d, err
 | |
| 	}
 | |
| 	if !f.Initialized() {
 | |
| 		return d, fmt.Errorf("%v is not initialized", f.Name)
 | |
| 	}
 | |
| 	return c.describer.Describe(ctx, f.Name)
 | |
| }
 | |
| 
 | |
| // List currently deployed Functions.
 | |
| func (c *Client) List(ctx context.Context) ([]ListItem, error) {
 | |
| 	// delegate to concrete implementation of lister entirely.
 | |
| 	return c.lister.List(ctx)
 | |
| }
 | |
| 
 | |
| // Remove a Function.  Name takes precidence.  If no name is provided,
 | |
| // the Function defined at root is used if it exists.
 | |
| func (c *Client) Remove(ctx context.Context, cfg Function, deleteAll bool) error {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 	// If name is provided, it takes precidence.
 | |
| 	// Otherwise load the Function defined at root.
 | |
| 	functionName := cfg.Name
 | |
| 	if cfg.Name == "" {
 | |
| 		f, err := NewFunction(cfg.Root)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if !f.Initialized() {
 | |
| 			return fmt.Errorf("Function at %v can not be removed unless initialized. Try removing by name", f.Root)
 | |
| 		}
 | |
| 		functionName = f.Name
 | |
| 		cfg = f
 | |
| 	}
 | |
| 
 | |
| 	// Delete Knative Service and dependent resources in parallel
 | |
| 	c.progressListener.Increment(fmt.Sprintf("Removing Knative Service: %v", functionName))
 | |
| 	errChan := make(chan error)
 | |
| 	go func() {
 | |
| 		errChan <- c.remover.Remove(ctx, functionName)
 | |
| 	}()
 | |
| 
 | |
| 	var errResources error
 | |
| 	if deleteAll {
 | |
| 		c.progressListener.Increment(fmt.Sprintf("Removing Knative Service '%v' and all dependent resources", functionName))
 | |
| 		errResources = c.pipelinesProvider.Remove(ctx, cfg)
 | |
| 	}
 | |
| 
 | |
| 	errService := <-errChan
 | |
| 
 | |
| 	if errService != nil && errResources != nil {
 | |
| 		return fmt.Errorf("%s\n%s", errService, errResources)
 | |
| 	} else if errResources != nil {
 | |
| 		return errResources
 | |
| 	}
 | |
| 	return errService
 | |
| }
 | |
| 
 | |
| // Invoke is a convenience method for triggering the execution of a Function
 | |
| // for testing and development.
 | |
| // The target argument is optional, naming the running instance of the Function
 | |
| // which should be invoked.  This can be the literal names "local" or "remote",
 | |
| // or can be a URL to an arbitrary endpoint.  If not provided, a running local
 | |
| // instance is preferred, with the remote Function triggered if there is no
 | |
| // locally running instance.
 | |
| // Example:
 | |
| //  myClient.Invoke(myContext, myFunction, "local", NewInvokeMessage())
 | |
| // The message sent to the Function is defined by the invoke message.
 | |
| // See NewInvokeMessage for its defaults.
 | |
| // Functions are invoked in a manner consistent with the settings defined in
 | |
| // their metadata.  For example HTTP vs CloudEvent
 | |
| func (c *Client) Invoke(ctx context.Context, root string, target string, m InvokeMessage) (s string, err error) {
 | |
| 	go func() {
 | |
| 		<-ctx.Done()
 | |
| 		c.progressListener.Stopping()
 | |
| 	}()
 | |
| 
 | |
| 	f, err := NewFunction(root)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// See invoke.go for implementation details
 | |
| 	return invoke(ctx, c, f, target, m)
 | |
| }
 | |
| 
 | |
| // Push the image for the named service to the configured registry
 | |
| func (c *Client) Push(ctx context.Context, path string) (err error) {
 | |
| 	f, err := NewFunction(path)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if !f.Built() {
 | |
| 		return ErrNotBuilt
 | |
| 	}
 | |
| 
 | |
| 	imageDigest, err := c.pusher.Push(ctx, f)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Record the Image Digest pushed.
 | |
| 	f.ImageDigest = imageDigest
 | |
| 	return f.Write()
 | |
| }
 | |
| 
 | |
| // DEFAULTS
 | |
| // ---------
 | |
| 
 | |
| // Manual implementations (noops) of required interfaces.
 | |
| // In practice, the user of this client package (for example the CLI) will
 | |
| // provide a concrete implementation for only the interfaces necessary to
 | |
| // complete the given command.  Integrators importing the package would
 | |
| // provide a concrete implementation for all interfaces to be used. To
 | |
| // enable partial definition (in particular used for testing) they
 | |
| // are defaulted to noop implementations such that they can be provded
 | |
| // only when necessary.  Unit tests for the concrete implementations
 | |
| // serve to keep the core logic here separate from the imperitive, and
 | |
| // with a minimum of external dependencies.
 | |
| // -----------------------------------------------------
 | |
| 
 | |
| // Builder
 | |
| type noopBuilder struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopBuilder) Build(ctx context.Context, _ Function) error { return nil }
 | |
| 
 | |
| // Pusher
 | |
| type noopPusher struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopPusher) Push(ctx context.Context, f Function) (string, error) { return "", nil }
 | |
| 
 | |
| // Deployer
 | |
| type noopDeployer struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopDeployer) Deploy(ctx context.Context, _ Function) (DeploymentResult, error) {
 | |
| 	return DeploymentResult{}, nil
 | |
| }
 | |
| 
 | |
| // Runner
 | |
| type noopRunner struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopRunner) Run(context.Context, Function) (job *Job, err error) {
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Remover
 | |
| type noopRemover struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopRemover) Remove(context.Context, string) error { return nil }
 | |
| 
 | |
| // Lister
 | |
| type noopLister struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopLister) List(context.Context) ([]ListItem, error) { return []ListItem{}, nil }
 | |
| 
 | |
| // Describer
 | |
| type noopDescriber struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopDescriber) Describe(context.Context, string) (Instance, error) {
 | |
| 	return Instance{}, errors.New("no describer provided")
 | |
| }
 | |
| 
 | |
| // PipelinesProvider
 | |
| type noopPipelinesProvider struct{}
 | |
| 
 | |
| func (n *noopPipelinesProvider) Run(ctx context.Context, _ Function) error    { return nil }
 | |
| func (n *noopPipelinesProvider) Remove(ctx context.Context, _ Function) error { return nil }
 | |
| 
 | |
| // DNSProvider
 | |
| type noopDNSProvider struct{ output io.Writer }
 | |
| 
 | |
| func (n *noopDNSProvider) Provide(_ Function) error { return nil }
 | |
| 
 | |
| // ProgressListener
 | |
| type NoopProgressListener struct{}
 | |
| 
 | |
| func (p *NoopProgressListener) SetTotal(i int)     {}
 | |
| func (p *NoopProgressListener) Increment(m string) {}
 | |
| func (p *NoopProgressListener) Complete(m string)  {}
 | |
| func (p *NoopProgressListener) Stopping()          {}
 | |
| func (p *NoopProgressListener) Done()              {}
 | |
| 
 | |
| // mkdir attempts to mkdir, writing any errors to stderr.
 | |
| func mkdir(path string) {
 | |
| 	// Since it is expected that the code elsewhere never assume directories
 | |
| 	// exist (doing so is a racing condition), it is valid to simply
 | |
| 	// handle errors at this level.
 | |
| 	if err := os.MkdirAll(path, 0700); err != nil {
 | |
| 		fmt.Fprintf(os.Stderr, "Error creating '%v': %v", path, err)
 | |
| 		debug.PrintStack()
 | |
| 	}
 | |
| }
 |