diff --git a/client.go b/client.go index c2579896b8..9f6025827d 100644 --- a/client.go +++ b/client.go @@ -1,7 +1,6 @@ package faas import ( - "errors" "fmt" "io" "io/ioutil" @@ -22,8 +21,7 @@ type Client struct { verbose bool // print verbose logs builder Builder // Builds a runnable image from Function source pusher Pusher // Pushes the image assocaited with a Function. - deployer Deployer // Deploys a Function - updater Updater // Updates a deployed Function + deployer Deployer // Deploys or Updates a Function runner Runner // Runs the Function locally remover Remover // Removes remote services lister Lister // Lists remote services @@ -53,12 +51,6 @@ type Deployer interface { Deploy(Function) error } -// Updater of a deployed Function with new image. -type Updater interface { - // Update a Function - Update(Function) error -} - // Runner runs the Function locally. type Runner interface { // Run the Function locally. @@ -124,7 +116,6 @@ func New(options ...Option) *Client { builder: &noopBuilder{output: os.Stdout}, pusher: &noopPusher{output: os.Stdout}, deployer: &noopDeployer{output: os.Stdout}, - updater: &noopUpdater{output: os.Stdout}, runner: &noopRunner{output: os.Stdout}, remover: &noopRemover{output: os.Stdout}, lister: &noopLister{output: os.Stdout}, @@ -171,13 +162,6 @@ func WithDeployer(d Deployer) Option { } } -// WithUpdater provides the concrete implementation of an updater. -func WithUpdater(u Updater) Option { - return func(c *Client) { - c.updater = u - } -} - // WithRunner provides the concrete implementation of a deployer. func WithRunner(r Runner) Option { return func(c *Client) { @@ -418,26 +402,24 @@ func (c *Client) Build(path string) (err error) { // Deploy the Function at path. Errors if the Function has not been // initialized with an image tag. func (c *Client) Deploy(path string) (err error) { + f, err := NewFunction(path) if err != nil { return } - if f.Image == "" { - return errors.New("Function needs to have Image tag calculated prior to building.") - } - err = c.pusher.Push(f) // First push the image to an image registry - if err != nil { + // Build the Function + if err = c.Build(f.Root); err != nil { return } - if err = c.deployer.Deploy(f); err != nil { + + // Push the image for the named service to the configured registry + if err = c.pusher.Push(f); err != nil { return } - if c.verbose { - // TODO: aspirational. Should be an actual route echo. - fmt.Printf("OK https://%v/\n", f.Image) - } - return + + // Deploy a new or Update the previously-deployed Function + return c.deployer.Deploy(f) } func (c *Client) Route(path string) (err error) { @@ -455,42 +437,6 @@ func (c *Client) Route(path string) (err error) { return c.dnsProvider.Provide(f) } -// Update a previously created Function. -func (c *Client) Update(root string) (err error) { - - // Create an instance of a Function representation at the given root. - f, err := NewFunction(root) - if err != nil { - return - } - - if !f.Initialized() { - // TODO: this needs a test. - return fmt.Errorf("the given path '%v' does not contain an initialized Function. Please create one at this path before updating.", root) - } - - // Build an image from the current state of the Function's implementation. - err = c.Build(f.Root) - if err != nil { - return - } - - // reload the Function as it will now have the Image populated if it had not yet been set. - f, err = NewFunction(f.Root) - if err != nil { - return - } - - // Push the image for the named service to the configured registry - if err = c.pusher.Push(f); err != nil { - return - } - - // Update the previously-deployed Function, returning its publicly - // addressible name for possible registration. - return c.updater.Update(f) -} - // Run the Function whose code resides at root. func (c *Client) Run(root string) error { @@ -573,10 +519,6 @@ type noopDeployer struct{ output io.Writer } func (n *noopDeployer) Deploy(_ Function) error { return nil } -type noopUpdater struct{ output io.Writer } - -func (n *noopUpdater) Update(_ Function) error { return nil } - type noopRunner struct{ output io.Writer } func (n *noopRunner) Run(_ Function) error { return nil } diff --git a/client_test.go b/client_test.go index e2e50d8d89..c9b82c67e8 100644 --- a/client_test.go +++ b/client_test.go @@ -460,7 +460,7 @@ func TestRun(t *testing.T) { } } -// TestUpdate ensures that the updater properly invokes the build/push/deploy +// TestUpdate ensures that the deployer properly invokes the build/push/deploy // process, erroring if run on a directory uncreated. func TestUpdate(t *testing.T) { var ( @@ -469,7 +469,7 @@ func TestUpdate(t *testing.T) { expectedImage = "quay.io/alice/testUpdate:latest" builder = mock.NewBuilder() pusher = mock.NewPusher() - updater = mock.NewUpdater() + deployer = mock.NewDeployer() ) // Create the root Function directory @@ -483,7 +483,7 @@ func TestUpdate(t *testing.T) { faas.WithRepository(TestRepository), faas.WithBuilder(builder), faas.WithPusher(pusher), - faas.WithUpdater(updater)) + faas.WithDeployer(deployer)) // create the new Function which will be updated if err := client.Create(faas.Function{Root: root}); err != nil { @@ -512,7 +512,7 @@ func TestUpdate(t *testing.T) { } // Update whose implementaiton verifed the expected name and image - updater.UpdateFn = func(f faas.Function) error { + deployer.DeployFn = func(f faas.Function) error { if f.Name != expectedName { t.Fatalf("updater expected name '%v', got '%v'", expectedName, f.Name) } @@ -524,7 +524,7 @@ func TestUpdate(t *testing.T) { // Invoke the creation, triggering the Function delegates, and // perform follow-up assertions that the Functions were indeed invoked. - if err := client.Update(root); err != nil { + if err := client.Deploy(root); err != nil { t.Fatal(err) } @@ -534,8 +534,8 @@ func TestUpdate(t *testing.T) { if !pusher.PushInvoked { t.Fatal("pusher was not invoked") } - if !updater.UpdateInvoked { - t.Fatal("updater was not invoked") + if !deployer.DeployInvoked { + t.Fatal("deployer was not invoked") } } diff --git a/cmd/create.go b/cmd/create.go index e13d1cba15..77235268cf 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -87,7 +87,10 @@ func runCreate(cmd *cobra.Command, args []string) (err error) { pusher := docker.NewPusher() pusher.Verbose = verbose - deployer := knative.NewDeployer() + deployer, err := knative.NewDeployer(config.Namespace) + if err != nil { + return + } deployer.Verbose = verbose listener := progress.New() @@ -107,7 +110,6 @@ func runCreate(cmd *cobra.Command, args []string) (err error) { type createConfig struct { initConfig - buildConfig deployConfig // Note that ambiguous references set to assume .initConfig } @@ -115,7 +117,6 @@ type createConfig struct { func newCreateConfig(args []string) createConfig { return createConfig{ initConfig: newInitConfig(args), - buildConfig: newBuildConfig(), deployConfig: newDeployConfig(), } } @@ -142,10 +143,10 @@ func (c createConfig) Prompt() createConfig { Trigger: prompt.ForString("Trigger", c.Trigger), // Templates intentionally omitted from prompt for being an edge case. }, - buildConfig: buildConfig{ - Repository: prompt.ForString("Repository for Function images", c.buildConfig.Repository), - }, deployConfig: deployConfig{ + buildConfig: buildConfig{ + Repository: prompt.ForString("Repository for Function images", c.buildConfig.Repository), + }, Namespace: prompt.ForString("Override default deploy namespace", c.Namespace), }, } diff --git a/cmd/deploy.go b/cmd/deploy.go index bec597d42c..c9724b4106 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -7,16 +7,20 @@ import ( "github.com/spf13/cobra" "github.com/boson-project/faas" + "github.com/boson-project/faas/buildpacks" "github.com/boson-project/faas/docker" "github.com/boson-project/faas/knative" + "github.com/boson-project/faas/progress" "github.com/boson-project/faas/prompt" ) func init() { root.AddCommand(deployCmd) deployCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options - $FAAS_CONFIRM") + deployCmd.Flags().StringP("image", "i", "", "Optional full image name, in form [registry]/[namespace]/[name]:[tag] for example quay.io/myrepo/project.name:latest (overrides --repository) - $FAAS_IMAGE") deployCmd.Flags().StringP("namespace", "n", "", "Override namespace into which the Function is deployed (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE") deployCmd.Flags().StringP("path", "p", cwd(), "Path to the function project directory - $FAAS_PATH") + deployCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. - $FAAS_REPOSITORY") } var deployCmd = &cobra.Command{ @@ -24,45 +28,88 @@ var deployCmd = &cobra.Command{ Short: "Deploy an existing Function project to a cluster", Long: `Deploy an existing Function project to a cluster -Deploys the Function project in the current directory. A path to the project -directory may be provided using the --path or -p flag. The image to be deployed -must have already been created using the "build" command. +Builds and Deploys the Function project in the current directory. +A path to the project directory may be provided using the --path or -p flag. +Reads the faas.yaml configuration file to determine the image name. +An image and repository may be specified on the command line using +the --image or -i and --repository or -r flag. + +If the Function is already deployed, it is updated with a new container image +that is pushed to an image repository, and the Knative Service is updated. The namespace into which the project is deployed defaults to the value in the faas.yaml configuration file. If NAMESPACE is not set in the configuration, the namespace currently active in the Kubernetes configuration file will be used. The namespace may be specified on the command line using the --namespace or -n flag, and if so this will overwrite the value in the faas.yaml file. + + `, SuggestFor: []string{"delpoy", "deplyo"}, - PreRunE: bindEnv("namespace", "path", "confirm"), + PreRunE: bindEnv("image", "namespace", "path", "repository", "confirm"), RunE: runDeploy, } func runDeploy(cmd *cobra.Command, _ []string) (err error) { - config := newDeployConfig() - function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace}) + + config := newDeployConfig().Prompt() + + function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image}) if err != nil { - return err + return } + + // Check if the Function has been initialized + if !function.Initialized() { + return fmt.Errorf("the given path '%v' does not contain an initialized Function. Please create one at this path before deploying.", config.Path) + } + + // If the Function does not yet have an image name and one was not provided on the command line if function.Image == "" { - return fmt.Errorf("Cannot determine the Function image name. Have you built it yet?") + // AND a --repository was not provided, then we need to + // prompt for a repository from which we can derive an image name. + if config.Repository == "" { + fmt.Print("A repository for Function images is required. For example, 'docker.io/tigerteam'.\n\n") + config.Repository = prompt.ForString("Repository for Function images", "") + if config.Repository == "" { + return fmt.Errorf("Unable to determine Function image name") + } + } + + // We have the repository, so let's use it to derive the Function image name + config.Image = deriveImage(config.Image, config.Repository, config.Path) + function.Image = config.Image } - // Confirm or print configuration - config.Prompt() + // All set, let's write changes in the config to the disk + err = function.WriteConfig() + if err != nil { + return + } + + builder := buildpacks.NewBuilder() + builder.Verbose = config.Verbose pusher := docker.NewPusher() pusher.Verbose = config.Verbose - deployer := knative.NewDeployer() + deployer, err := knative.NewDeployer(config.Namespace) + if err != nil { + return + } + + listener := progress.New() + deployer.Verbose = config.Verbose deployer.Namespace = function.Namespace client := faas.New( faas.WithVerbose(config.Verbose), + faas.WithRepository(config.Repository), // for deriving image name when --image not provided explicitly. + faas.WithBuilder(builder), faas.WithPusher(pusher), - faas.WithDeployer(deployer)) + faas.WithDeployer(deployer), + faas.WithProgressListener(listener)) return client.Deploy(config.Path) @@ -71,6 +118,8 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) { } type deployConfig struct { + buildConfig + // Namespace override for the deployed function. If provided, the // underlying platform will be instructed to deploy the function to the given // namespace (if such a setting is applicable; such as for Kubernetes @@ -95,10 +144,11 @@ type deployConfig struct { // environment variables; in that precedence. func newDeployConfig() deployConfig { return deployConfig{ - Namespace: viper.GetString("namespace"), - Path: viper.GetString("path"), - Verbose: viper.GetBool("verbose"), // defined on root - Confirm: viper.GetBool("confirm"), + buildConfig: newBuildConfig(), + Namespace: viper.GetString("namespace"), + Path: viper.GetString("path"), + Verbose: viper.GetBool("verbose"), // defined on root + Confirm: viper.GetBool("confirm"), } } @@ -109,9 +159,16 @@ func (c deployConfig) Prompt() deployConfig { if !interactiveTerminal() || !c.Confirm { return c } - return deployConfig{ + dc := deployConfig{ + buildConfig: buildConfig{ + Repository: prompt.ForString("Repository for Function images", c.buildConfig.Repository), + }, Namespace: prompt.ForString("Namespace", c.Namespace), Path: prompt.ForString("Project path", c.Path), Verbose: c.Verbose, } + + dc.Image = deriveImage(dc.Image, dc.Repository, dc.Path) + + return dc } diff --git a/cmd/root.go b/cmd/root.go index 9ac08f03a2..da221e4cf0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -128,17 +128,18 @@ type functionOverrides struct { // functionWithOverrides sets the namespace and image strings for the // Function project at root, if provided, and returns the Function -// configuration values +// configuration values. +// Please note that When this function is called, the overrides are not persisted. func functionWithOverrides(root string, overrides functionOverrides) (f faas.Function, err error) { f, err = faas.NewFunction(root) if err != nil { return } - overrideMapping := []struct{ + overrideMapping := []struct { src string dest *string - } { + }{ {overrides.Builder, &f.Builder}, {overrides.Image, &f.Image}, {overrides.Namespace, &f.Namespace}, @@ -150,14 +151,7 @@ func functionWithOverrides(root string, overrides functionOverrides) (f faas.Fun } } - err = f.WriteConfig() - if err != nil { - return - } - - f, err = faas.NewFunction(root) return - } // deriveName returns the explicit value (if provided) or attempts to derive diff --git a/cmd/update.go b/cmd/update.go deleted file mode 100644 index f9d5850d9d..0000000000 --- a/cmd/update.go +++ /dev/null @@ -1,125 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/ory/viper" - "github.com/spf13/cobra" - - "github.com/boson-project/faas" - "github.com/boson-project/faas/buildpacks" - "github.com/boson-project/faas/docker" - "github.com/boson-project/faas/knative" - "github.com/boson-project/faas/prompt" -) - -func init() { - root.AddCommand(updateCmd) - updateCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options - $FAAS_CONFIRM") - updateCmd.Flags().StringP("namespace", "n", "", "Override namespace for the Function (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE") - updateCmd.Flags().StringP("path", "p", cwd(), "Path to the Function project directory - $FAAS_PATH") - updateCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. - $FAAS_REPOSITORY") -} - -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Update a deployed Function", - Long: `Update a deployed Function - -Updates the deployed Function project in the current directory or in the -directory specified by the --path flag. Reads the faas.yaml configuration file -to determine the image name. - -The deployed Function is updated with a new container image that is pushed to a -container image repository, and the Knative Service is updated. - -The namespace defaults to the value in faas.yaml or the namespace currently -active in the user Kubernetes configuration. The namespace may be specified on -the command line, and if so this will overwrite the value in faas.yaml. - -An image repository may be specified on the command line using the --repository -or -r flag. - -Note that the behavior of update is different than that of deploy and run. When -update is run, a new container image is always built. -`, - SuggestFor: []string{"push", "deploy"}, - PreRunE: bindEnv("namespace", "path", "repository", "confirm"), - RunE: runUpdate, -} - -func runUpdate(cmd *cobra.Command, args []string) (err error) { - config := newUpdateConfig() - function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace}) - if err != nil { - return err - } - if function.Image == "" { - return fmt.Errorf("Cannot determine the Function image. Have you built it yet?") - } - config.Prompt() - - builder := buildpacks.NewBuilder() - builder.Verbose = config.Verbose - - pusher := docker.NewPusher() - pusher.Verbose = config.Verbose - - updater, err := knative.NewUpdater(config.Namespace) - if err != nil { - return - } - updater.Verbose = config.Verbose - - client := faas.New( - faas.WithVerbose(config.Verbose), - faas.WithBuilder(builder), - faas.WithPusher(pusher), - faas.WithUpdater(updater)) - - return client.Update(config.Path) -} - -type updateConfig struct { - // Namespace override for the deployed Function. If provided, the - // underlying platform will be instructed to deploy the Function to the given - // namespace (if such a setting is applicable; such as for Kubernetes - // clusters). If not provided, the currently configured namespace will be - // used. For instance, that which would be used by default by `kubectl` - // (~/.kube/config) in the case of Kubernetes. - Namespace string - - // Path of the Function implementation on local disk. Defaults to current - // working directory of the process. - Path string - - // Repository at which interstitial build artifacts should be kept. - // Registry is optional and is defaulted to faas.DefaultRegistry. - // ex: "quay.io/myrepo" or "myrepo" - // This setting is ignored if Image is specified, which includes the full - Repository string - - // Verbose logging. - Verbose bool -} - -func newUpdateConfig() updateConfig { - return updateConfig{ - Namespace: viper.GetString("namespace"), - Path: viper.GetString("path"), - Repository: viper.GetString("repository"), - Verbose: viper.GetBool("verbose"), // defined on root - } -} - -func (c updateConfig) Prompt() updateConfig { - if !interactiveTerminal() || !viper.GetBool("confirm") { - return c - } - return updateConfig{ - Namespace: prompt.ForString("Namespace", c.Namespace), - Path: prompt.ForString("Project path", c.Path), - Verbose: c.Verbose, - } - -} diff --git a/docs/commands.md b/docs/commands.md index 8576553038..d56e609bef 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -52,40 +52,25 @@ kn faas run ## `deploy` -Deploys the Function project in the current directory. The user may specify a path to the project directory as a flag. Reads the `faas.yaml` configuration file to determine the image name. Derives the service name from the project name. There is no command line option to specify the image name, although this can be changed in `faas.yaml`. There is no mechanism by which the user can specify the service name. The user must have already built an image for this function using `faas deploy` or they will encounter an error. +Builds and deploys the Function project in the current directory. The user may specify a path to the project directory using the `--path` or `-p` flag. Reads the `faas.yaml` configuration file to determine the image name. An image and repository may be specified on the command line using the `--image` or `-i` and `--repository` or `-r` flag. -The namespace defaults to the value in `faas.yaml` or the namespace currently active in the user's Kubernetes configuration. The namespace may be specified on the command line, and if so this will overwrite the value in `faas.yaml`. +Derives the service name from the project name. There is no mechanism by which the user can specify the service name. The user must have already initialized the function using `faas init` or they will encounter an error. -Similar `kn` command: `kn service create NAME --image IMAGE [flags]`. This command allows a user to deploy a Knative Service by specifying an image, typically one hosted on a public container registry such as docker.io. The deployment options which the `kn` command affords the user are quite broad. The `kn` command in this case is quite effective for a power user. The `faas deploy` command has a similar end result, but is definitely easier for a user just getting started to be successful with. +If the Function is already deployed, it is updated with a new container image that is pushed to a +container image repository, and the Knative Service is updated. -```console -faas deploy [-n -p ] -``` +The namespace into which the project is deployed defaults to the value in the `faas.yaml` configuration file. If `NAMESPACE` is not set in the configuration, the namespace currently active in the Kubernetes configuration file will be used. The namespace may be specified on the command line using the `--namespace` or `-n` flag, and if so this will overwrite the value in the `faas.yaml` file. -When run as a `kn` plugin. - -```console -kn faas deploy [-n -p ] -``` - -## `update` - -Updates the deployed Function project in the current directory. The user may specify the path on the command line with a flag. Reads the `faas.yaml` configuration file to determine the image name. Derives the service name from the project name. The deployed Function is updated with a new container image that is pushed to a user repository, and the Knative `Service` is then updated. - -The namespace defaults to the value in `faas.yaml` or the namespace currently active in the user's Kubernetes configuration. The namespace may be specified on the command line, and if so this will overwrite the value in `faas.yaml`. The user may specify a repository on the command line. - -Note that the behavior of `update` is different than that of `deploy` and `run`. When `update` is run, a new container image is always built. However, for `deploy` and `run`, the user is required to run `faas build` first. The `update` command also differs from `deploy` in that it allows the user to specify a repository on the command line (but still not an image name). Consider normalizing all of this so that all of these commands behave similarly. - -Similar `kn` command: `kn service update NAME [flags]`. As with `deploy`, the `update` command provides a level of simplicity for a new user that restricts flexibility while improving the ease of use. +Similar `kn` command: `kn service create NAME --image IMAGE [flags]`. This command allows a user to deploy a Knative Service by specifying an image, typically one hosted on a public container registry such as docker.io. The deployment options which the `kn` command affords the user are quite broad. The `kn` command in this case is quite effective for a power user. The `faas deploy` command has a similar end result, but is definitely easier for a user just getting started to be successful with. ```console -faas update [-r -p ] +faas deploy [-n -p -i -r ] ``` When run as a `kn` plugin. ```console -kn faas update [-r -p ] +kn faas deploy [-n -p -i -r ] ``` ## `describe` diff --git a/function.go b/function.go index 6d96ff782a..7df37fd43e 100644 --- a/function.go +++ b/function.go @@ -43,7 +43,7 @@ type Function struct { // Builder represents the CNCF Buildpack builder image for a function, // or it might be reference to `BuilderMap`. - Builder string + Builder string // Map containing known builders. // e.g. { "jvm": "docker.io/example/quarkus-jvm-builder" } @@ -94,7 +94,8 @@ func (f Function) Initialized() bool { if err != nil { return false } - return c.Name != "" // TODO: use a dedicated initialized bool? + + return c.Runtime != "" && c.Name != "" } // DerivedImage returns the derived image name (OCI container tag) of the diff --git a/knative/client.go b/knative/client.go new file mode 100644 index 0000000000..dca09c4eac --- /dev/null +++ b/knative/client.go @@ -0,0 +1,43 @@ +package knative + +import ( + "bytes" + "fmt" + "io" + "os" + "time" + + "k8s.io/client-go/tools/clientcmd" + "knative.dev/client/pkg/kn/commands" + clientservingv1 "knative.dev/client/pkg/serving/v1" +) + +const ( + DefaultWaitingTimeout = 60 * time.Second +) + +func NewClient(namespace string, verbose bool) (clientservingv1.KnServingClient, io.Writer, error) { + + p := commands.KnParams{} + p.Initialize() + + // Capture output in a buffer if verbose is not enabled for output on error. + if verbose { + p.Output = os.Stdout + } else { + p.Output = &bytes.Buffer{} + } + + if namespace == "" { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + namespace, _, _ = clientConfig.Namespace() + } + + client, err := p.NewServingClient(namespace) + if err != nil { + return nil, p.Output, fmt.Errorf("failed to create new serving client: %v", err) + } + + return client, p.Output, nil +} diff --git a/knative/deployer.go b/knative/deployer.go index 7b3b6d2b64..d41d2dab64 100644 --- a/knative/deployer.go +++ b/knative/deployer.go @@ -3,19 +3,20 @@ package knative import ( "bytes" "fmt" - "io" - "os" + "sort" + "time" - "knative.dev/client/pkg/kn/root" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/client/pkg/wait" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + v1 "knative.dev/serving/pkg/apis/serving/v1" "github.com/boson-project/faas" "github.com/boson-project/faas/k8s" ) -// TODO: Use knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1 -// NewForConfig gives you the client, and then you can do -// client.Services("ns").Get("name") - type Deployer struct { // Namespace with which to override that set on the default configuration (such as the ~/.kube/config). // If left blank, deployment will commence to the configured namespace. @@ -24,52 +25,151 @@ type Deployer struct { Verbose bool } -func NewDeployer() *Deployer { - return &Deployer{} +func NewDeployer(namespaceOverride string) (deployer *Deployer, err error) { + deployer = &Deployer{} + _, namespace, err := newClientConfig(namespaceOverride) + if err != nil { + return + } + deployer.Namespace = namespace + // deployer.client, err = servingv1client.NewForConfig(config) + return } func (d *Deployer) Deploy(f faas.Function) (err error) { - // k8s does not support service names with dots. so encode it such that + + // k8s does not support service names with dots. so encode it such that // www.my-domain,com -> www-my--domain-com encodedName, err := k8s.ToK8sAllowedName(f.Name) if err != nil { return } - // Capture output in a buffer if verbose is not enabled for output on error. - var output io.Writer - if d.Verbose { - output = os.Stdout - } else { - output = &bytes.Buffer{} + client, output, err := NewClient(d.Namespace, d.Verbose) + if err != nil { + return } - c, err := root.NewRootCommand(nil) + _, err = client.GetService(encodedName) if err != nil { - return err + if errors.IsNotFound(err) { + + // Let's create a new Service + err := client.CreateService(generateNewService(encodedName, f.Image)) + if err != nil { + if !d.Verbose { + err = fmt.Errorf("failed to deploy the service: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) + } else { + err = fmt.Errorf("failed to deploy the service: %v", err) + } + return err + } + + err, _ = client.WaitForService(encodedName, DefaultWaitingTimeout, wait.NoopMessageCallback()) + if err != nil { + if !d.Verbose { + err = fmt.Errorf("deployer failed to wait for the service to become ready: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) + } else { + err = fmt.Errorf("deployer failed to wait for the service to become ready: %v", err) + } + return err + } + + route, err := client.GetRoute(encodedName) + if err != nil { + if !d.Verbose { + err = fmt.Errorf("deployer failed to get the route: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) + } else { + err = fmt.Errorf("deployer failed to get the route: %v", err) + } + return err + } + + fmt.Println("Function deployed on: " + route.Status.URL.String()) + + } else { + if !d.Verbose { + err = fmt.Errorf("deployer failed to get the service: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) + } else { + err = fmt.Errorf("deployer failed to get the service: %v", err) + } + return err + } + } else { + // Update the existing Service + err = client.UpdateServiceWithRetry(encodedName, updateBuiltTimeStampEnvVar, 3) + if err != nil { + if !d.Verbose { + err = fmt.Errorf("deployer failed to update the service: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) + } else { + err = fmt.Errorf("deployer failed to update the service: %v", err) + } + return err + } } - c.SetOut(output) - args := []string{ - "service", "create", encodedName, - "--image", f.Image, - "--env", "VERBOSE=true", - "--label", "bosonFunction=true", + + return nil +} + +func generateNewService(name, image string) *servingv1.Service { + containers := []corev1.Container{ + { + Image: image, + Env: []corev1.EnvVar{ + {Name: "VERBOSE", Value: "true"}, + }, + }, } - if d.Namespace != "" { - args = append(args, "--namespace", d.Namespace) + + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "bosonFunction": "true", + }, + }, + Spec: v1.ServiceSpec{ + ConfigurationSpec: v1.ConfigurationSpec{ + Template: v1.RevisionTemplateSpec{ + Spec: v1.RevisionSpec{ + PodSpec: corev1.PodSpec{ + Containers: containers, + }, + }, + }, + }, + }, } - c.SetArgs(args) - err = c.Execute() - if err != nil { - if !d.Verbose { - err = fmt.Errorf("failed to deploy the service: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String()) - } else { - err = fmt.Errorf("failed to deploy the service: %v", err) +} + +func updateBuiltTimeStampEnvVar(service *servingv1.Service) (*servingv1.Service, error) { + envs := service.Spec.Template.Spec.Containers[0].Env + + builtEnvVarName := "BUILT" + + builtEnvVar := findEnvVar(builtEnvVarName, envs) + if builtEnvVar == nil { + envs = append(envs, corev1.EnvVar{Name: "VERBOSE", Value: "true"}) + builtEnvVar = &envs[len(envs)-1] + } + + builtEnvVar.Value = time.Now().Format("20060102T150405") + + sort.SliceStable(envs, func(i, j int) bool { + return envs[i].Name <= envs[j].Name + }) + service.Spec.Template.Spec.Containers[0].Env = envs + + return service, nil +} + +func findEnvVar(name string, envs []corev1.EnvVar) *corev1.EnvVar { + var result *corev1.EnvVar = nil + for i, envVar := range envs { + if envVar.Name == name { + result = &envs[i] + break } - return } - // TODO: use the KN service client noted above, such that we can return the - // final path/route of the final deployed Function. While it can be assumed - // due to being deterministic, new users would be aided by having it echoed. - return + return result } diff --git a/knative/updater.go b/knative/updater.go deleted file mode 100644 index ad2cea04ec..0000000000 --- a/knative/updater.go +++ /dev/null @@ -1,142 +0,0 @@ -package knative - -import ( - "bytes" - "fmt" - "html/template" - "math/rand" - "sort" - "strings" - "time" - - . "k8s.io/api/core/v1" - apiMachineryV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - servingv1 "knative.dev/serving/pkg/apis/serving/v1" - servingV1client "knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1" - - "github.com/boson-project/faas" - "github.com/boson-project/faas/k8s" -) - -type Updater struct { - Verbose bool - namespace string - client *servingV1client.ServingV1Client -} - -func NewUpdater(namespaceOverride string) (updater *Updater, err error) { - updater = &Updater{} - config, namespace, err := newClientConfig(namespaceOverride) - if err != nil { - return - } - updater.namespace = namespace - updater.client, err = servingV1client.NewForConfig(config) - return -} - -func (updater *Updater) Update(f faas.Function) error { - client, namespace := updater.client, updater.namespace - - project, err := k8s.ToK8sAllowedName(f.Name) - if err != nil { - return fmt.Errorf("updater call to k8s.ToK8sAllowedName failed: %v", err) - } - - service, err := client.Services(namespace).Get(project, apiMachineryV1.GetOptions{}) - if err != nil { - return fmt.Errorf("updater failed to get the service: %v", err) - } - - if service.Spec.Template.Spec.Containers == nil || len(service.Spec.Template.Spec.Containers) < 1 { - return fmt.Errorf("updater failed to find the container for the service") - } - - container := &service.Spec.Template.Spec.Containers[0] - updateBuiltTimeStampEnvVar(container) - - service.Spec.Template.Name, err = generateRevisionName("{{.Service}}-{{.Random 5}}-{{.Generation}}", service) - if err != nil { - return fmt.Errorf("updater failed to generate revision name: %v", err) - } - - _, err = client.Services(namespace).Update(service) - if err != nil { - return fmt.Errorf("updater failed to update the service: %v", err) - } - - return nil -} - -func updateBuiltTimeStampEnvVar(container *Container) { - builtEnvVarName := "BUILT" - envs := container.Env - - builtEnvVar := findEnvVar(builtEnvVarName, envs) - if builtEnvVar == nil { - envs = append(envs, EnvVar{Name: builtEnvVarName}) - builtEnvVar = &envs[len(envs)-1] - } - - builtEnvVar.Value = time.Now().Format("20060102T150405") - - sort.SliceStable(envs, func(i, j int) bool { - return envs[i].Name <= envs[j].Name - }) - container.Env = envs -} - -func findEnvVar(name string, envs []EnvVar) *EnvVar { - var result *EnvVar = nil - for i, envVar := range envs { - if envVar.Name == name { - result = &envs[i] - break - } - } - return result -} - -var charChoices = []string{ - "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", - "y", "z", -} - -type revisionTemplContext struct { - Service string - Generation int64 -} - -func (c *revisionTemplContext) Random(l int) string { - chars := make([]string, 0, l) - for i := 0; i < l; i++ { - chars = append(chars, charChoices[rand.Int()%len(charChoices)]) - } - return strings.Join(chars, "") -} - -func generateRevisionName(nameTempl string, service *servingv1.Service) (string, error) { - templ, err := template.New("revisionName").Parse(nameTempl) - if err != nil { - return "", err - } - context := &revisionTemplContext{ - Service: service.Name, - Generation: service.Generation + 1, - } - buf := new(bytes.Buffer) - err = templ.Execute(buf, context) - if err != nil { - return "", err - } - res := buf.String() - // Empty is ok. - if res == "" { - return res, nil - } - prefix := service.Name + "-" - if !strings.HasPrefix(res, prefix) { - res = prefix + res - } - return res, nil -} diff --git a/mock/updater.go b/mock/updater.go deleted file mode 100644 index 13fff77398..0000000000 --- a/mock/updater.go +++ /dev/null @@ -1,19 +0,0 @@ -package mock - -import "github.com/boson-project/faas" - -type Updater struct { - UpdateInvoked bool - UpdateFn func(faas.Function) error -} - -func NewUpdater() *Updater { - return &Updater{ - UpdateFn: func(faas.Function) error { return nil }, - } -} - -func (i *Updater) Update(f faas.Function) error { - i.UpdateInvoked = true - return i.UpdateFn(f) -}