mirror of https://github.com/knative/func.git
updater: add kn-based implementation
This commit is contained in:
parent
3656f532b6
commit
fe12839e97
60
README.md
60
README.md
|
@ -2,44 +2,22 @@
|
|||
|
||||
Function as a Service CLI
|
||||
|
||||
## Requirements
|
||||
## Setup and Configuration
|
||||
|
||||
Go 1.13+
|
||||
|
||||
## Install
|
||||
|
||||
Build and install the resultant binary.
|
||||
With Go 1.13+ installed, build and install the binary to your path:
|
||||
```
|
||||
go install
|
||||
```
|
||||
|
||||
## Build
|
||||
Install dependent binaries:
|
||||
|
||||
Build binary into the local directory.
|
||||
```shell
|
||||
go build
|
||||
```
|
||||
## Usage
|
||||
* `kn` https://github.com/knative/client/releases
|
||||
* `kubectl` https://kubernetes.io/docs/tasks/tools/install-kubectl/
|
||||
* `docker` https://docs.docker.com/get-docker/
|
||||
|
||||
See help:
|
||||
```shell
|
||||
faas
|
||||
```
|
||||
Configure Image repository:
|
||||
|
||||
## Configuration
|
||||
|
||||
### Cluster Prerequisites
|
||||
|
||||
see https://github.com/lkingland/config for cluster setup and configuration. Broadly, requirements are:
|
||||
* Kubernetes
|
||||
* Knative Serving and Eventing
|
||||
* Knative Domains patched to enable domains
|
||||
* Knative Network patched to enable subdomains
|
||||
* Kourier
|
||||
|
||||
### Container Registry
|
||||
|
||||
Both the image registry and user/org namespace need to be defined either by
|
||||
Both the image repository and user/org namespace need to be defined either by
|
||||
using the --registry and --namespace flags on the `create` command, or by
|
||||
configuring as environment variables. For example to configure all images
|
||||
to be pushed to `quay.io/alice`, use:
|
||||
|
@ -48,15 +26,33 @@ export FAAS_REGISTRY=quay.io
|
|||
export FAAS_NAMESPACE=alice
|
||||
```
|
||||
|
||||
Cluster connection:
|
||||
|
||||
It is expected that kubectl and kn be configured to connect to a kubernetes cluster with the following configuration:
|
||||
|
||||
* Knative Serving and Eventing
|
||||
* Knative Domains patched to enable your chosen domain
|
||||
* Knative Network patched to enable subdomains
|
||||
* Kourier
|
||||
* Cert-manager
|
||||
|
||||
see https://github.com/lkingland/config for cluster setup and configuration details.
|
||||
|
||||
## Usage
|
||||
|
||||
See help:
|
||||
```shell
|
||||
faas
|
||||
```
|
||||
## Examples
|
||||
|
||||
Create a new Function Service:
|
||||
Create a new Service Function:
|
||||
|
||||
```shell
|
||||
> mkdir -p example.com/www
|
||||
> cd example.com/www
|
||||
> faas create go
|
||||
OK www.example.com
|
||||
https://www.example.com
|
||||
> curl https://www.example.com
|
||||
OK
|
||||
```
|
||||
|
|
|
@ -30,7 +30,7 @@ func NewInitializer() *Initializer {
|
|||
return &Initializer{}
|
||||
}
|
||||
|
||||
// Initialize a new funciton of the given name, of the given language, at the given path.
|
||||
// Initialize a new function of the given name, of the given language, at the given path.
|
||||
func (n *Initializer) Initialize(name, language, path string) error {
|
||||
// Check for the appsody binary explicitly so that we can return
|
||||
// an extra-friendly error message.
|
||||
|
|
|
@ -28,7 +28,7 @@ func TestInitialize(t *testing.T) {
|
|||
t.Skip()
|
||||
}
|
||||
|
||||
// Create the directory in which the function service will be initialized,
|
||||
// Create the directory in which the service function will be initialized,
|
||||
// removing it on test completion.
|
||||
if err := os.Mkdir("testdata/example.com/www", 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -14,7 +14,7 @@ func TestRun(t *testing.T) {
|
|||
t.Skip()
|
||||
}
|
||||
|
||||
// Testdata Function Service
|
||||
// Testdata Service Function
|
||||
//
|
||||
// The directory has been pre-populated with a runnable base function by running
|
||||
// init and committing the result, such that this test is not dependent on a
|
||||
|
|
181
client/client.go
181
client/client.go
|
@ -24,8 +24,9 @@ type Client struct {
|
|||
builder Builder // Builds a runnable image from function source
|
||||
pusher Pusher // Pushes a built image to a registry
|
||||
deployer Deployer // Deploys a Service Function
|
||||
updater Updater // Updates a deployed Service Function
|
||||
runner Runner // Runs the function locally
|
||||
remover Remover // Removes remote services.
|
||||
remover Remover // Removes remote services
|
||||
}
|
||||
|
||||
// DNSProvider exposes DNS services necessary for serving the Service Function.
|
||||
|
@ -43,14 +44,14 @@ type Initializer interface {
|
|||
|
||||
// Builder of function source to runnable image.
|
||||
type Builder interface {
|
||||
// Build a function service of the given name with source located at path.
|
||||
// Build a service function of the given name with source located at path.
|
||||
// returns the image name built.
|
||||
Build(name, path string) (image string, err error)
|
||||
}
|
||||
|
||||
// Pusher of function image to a registry.
|
||||
type Pusher interface {
|
||||
// Push the image of the function service.
|
||||
// Push the image of the service function.
|
||||
Push(image string) error
|
||||
}
|
||||
|
||||
|
@ -60,6 +61,12 @@ type Deployer interface {
|
|||
Deploy(name, image string) (address string, err error)
|
||||
}
|
||||
|
||||
// Updater of a deployed service function with new image.
|
||||
type Updater interface {
|
||||
// Deploy a service function of given name, using given backing image.
|
||||
Update(name, image string) error
|
||||
}
|
||||
|
||||
// Runner runs the function locally.
|
||||
type Runner interface {
|
||||
// Run the function locally.
|
||||
|
@ -148,6 +155,13 @@ 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) {
|
||||
|
@ -168,27 +182,29 @@ func WithRemover(r Remover) Option {
|
|||
func New(options ...Option) (c *Client, err error) {
|
||||
// Client with defaults overridden by optional parameters
|
||||
c = &Client{
|
||||
dnsProvider: &noopDNSProvider{output: os.Stdout},
|
||||
initializer: &noopInitializer{output: os.Stdout},
|
||||
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},
|
||||
domainSearchLimit: -1, // no recursion limit deriving domain by default.
|
||||
dnsProvider: &manualDNSProvider{output: os.Stdout},
|
||||
initializer: &manualInitializer{output: os.Stdout},
|
||||
builder: &manualBuilder{output: os.Stdout},
|
||||
pusher: &manualPusher{output: os.Stdout},
|
||||
deployer: &manualDeployer{output: os.Stdout},
|
||||
runner: &manualRunner{output: os.Stdout},
|
||||
remover: &manualRemover{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.
|
||||
// Working Directory
|
||||
// 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
|
||||
// Service Name
|
||||
// If not explicity set via the WithName option, we attempt to derive the
|
||||
// name from the effective root path.
|
||||
if c.name == "" {
|
||||
|
@ -263,15 +279,60 @@ func (c *Client) Create(language string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
// Dervive the cluster address of the service.
|
||||
// Derive the public domain of the service from the directory path.
|
||||
// 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.
|
||||
c.dnsProvider.Provide(c.name, address)
|
||||
|
||||
// Associate the public domain to the cluster-defined address.
|
||||
return
|
||||
}
|
||||
|
||||
// Update a previously created service function.
|
||||
func (c *Client) Update() (err error) {
|
||||
|
||||
// TODO: detect and error if `create` was never run, failed, or the
|
||||
// service is othewise un-updatable.
|
||||
|
||||
// Build an image from the current state of the service function's codebase.
|
||||
image, err := c.builder.Build(c.name, c.root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Push the image for the named service to the configured registry
|
||||
if err = c.pusher.Push(image); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the previously-deployed service function, returning its publicly
|
||||
// addressible name for possible registration.
|
||||
return c.updater.Update(c.name, image)
|
||||
}
|
||||
|
||||
// Run the function whose code resides at root.
|
||||
func (c *Client) Run() error {
|
||||
// delegate to concrete implementation of runner entirely.
|
||||
return c.runner.Run(c.root)
|
||||
}
|
||||
|
||||
// Remove a function from remote, bringing the service funciton
|
||||
// to the same state as if it had been created --local only.
|
||||
// Name is optional, as the presently associated service function
|
||||
// is inferred, but a client is allowed to remove any service
|
||||
// function for which the user has permission to remove, as this
|
||||
// is used for repairing broken local->remote associations.
|
||||
func (c *Client) Remove(name string) error {
|
||||
if name == "" {
|
||||
name = c.name
|
||||
}
|
||||
// delegate to concrete implementation of remover entirely.
|
||||
return c.remover.Remove(name)
|
||||
}
|
||||
|
||||
// Convert a path to a domain.
|
||||
// Searches up the path string until a domain (TLD+1) is detected.
|
||||
// Subdirectories are considered subdomains.
|
||||
|
@ -352,26 +413,6 @@ func pathToDomain(path string, maxLevels int) string {
|
|||
return domain
|
||||
}
|
||||
|
||||
// Run the function whose code resides at root.
|
||||
func (c *Client) Run() error {
|
||||
// delegate to concrete implementation of runner entirely.
|
||||
return c.runner.Run(c.root)
|
||||
}
|
||||
|
||||
// Remove a function from remote, bringing the service funciton
|
||||
// to the same state as if it had been created --local only.
|
||||
// Name is optional, as the presently associated service function
|
||||
// is inferred, but a client is allowed to remove any service
|
||||
// function for which the user has permission to remove, as this
|
||||
// is used for repairing broken local->remote associations.
|
||||
func (c *Client) Remove(name string) error {
|
||||
if name == "" {
|
||||
name = c.name
|
||||
}
|
||||
// delegate to concrete implementation of remover entirely.
|
||||
return c.remover.Remove(name)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -380,68 +421,58 @@ func (c *Client) Remove(name string) error {
|
|||
// serve to keep the core logic here separate from the imperitive.
|
||||
// -----------------------------------------------------
|
||||
|
||||
type manualDNSProvider struct {
|
||||
output io.Writer
|
||||
type noopDNSProvider struct{ output io.Writer }
|
||||
|
||||
func (p *noopDNSProvider) Provide(name, address string) {
|
||||
fmt.Fprintln(p.output, "skipping DNS update: client not initialized WithDNSProvider")
|
||||
}
|
||||
|
||||
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 noopInitializer struct{ output io.Writer }
|
||||
|
||||
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)
|
||||
func (i *noopInitializer) Initialize(name, language, root string) error {
|
||||
fmt.Fprintln(i.output, "skipping initialize: client not initialized WithInitializer")
|
||||
return nil
|
||||
}
|
||||
|
||||
type manualBuilder struct {
|
||||
output io.Writer
|
||||
}
|
||||
type noopBuilder struct{ output io.Writer }
|
||||
|
||||
func (i *manualBuilder) Build(name, root string) (image string, err error) {
|
||||
fmt.Fprintf(i.output, "Please manually build image for '%v' using code at '%v'\n", name, root)
|
||||
func (i *noopBuilder) Build(name, root string) (image string, err error) {
|
||||
fmt.Fprintln(i.output, "skipping build: client not initialized WithBuilder")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type manualPusher struct {
|
||||
output io.Writer
|
||||
}
|
||||
type noopPusher struct{ output io.Writer }
|
||||
|
||||
func (i *manualPusher) Push(image string) error {
|
||||
fmt.Fprintf(i.output, "Please manually push image '%v'\n", image)
|
||||
func (i *noopPusher) Push(image string) error {
|
||||
fmt.Fprintln(i.output, "skipping push: client not initialized WithPusher")
|
||||
return nil
|
||||
}
|
||||
|
||||
type manualDeployer struct {
|
||||
output io.Writer
|
||||
}
|
||||
type noopDeployer struct{ output io.Writer }
|
||||
|
||||
func (i *manualDeployer) Deploy(name, image string) (string, error) {
|
||||
fmt.Fprintf(i.output, "Please manually deploy '%v'\n", name)
|
||||
func (i *noopDeployer) Deploy(name, image string) (string, error) {
|
||||
fmt.Fprintln(i.output, "skipping deploy: client not initialized WithDeployer")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type manualRunner struct {
|
||||
output io.Writer
|
||||
}
|
||||
type noopUpdater struct{ output io.Writer }
|
||||
|
||||
func (i *manualRunner) Run(root string) error {
|
||||
fmt.Fprintf(i.output, "Please manually run using code at '%v'\n", root)
|
||||
func (i *noopUpdater) Update(name, image string) error {
|
||||
fmt.Fprintln(i.output, "skipping deploy: client not initialized WithDeployer")
|
||||
return nil
|
||||
}
|
||||
|
||||
type manualRemover struct {
|
||||
output io.Writer
|
||||
type noopRunner struct{ output io.Writer }
|
||||
|
||||
func (i *noopRunner) Run(root string) error {
|
||||
fmt.Fprintln(i.output, "skipping run: client not initialized WithRunner")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *manualRemover) Remove(name string) error {
|
||||
fmt.Fprintf(i.output, "Please manually remove service '%v'\n", name)
|
||||
type noopRemover struct{ output io.Writer }
|
||||
|
||||
func (i *noopRemover) Remove(name string) error {
|
||||
fmt.Fprintln(i.output, "skipping remove: client not initialized WithRemover")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -55,12 +55,12 @@ func TestCreate(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create a Function Service call missing language should error
|
||||
// create a Service Function call missing language should error
|
||||
if err := client.Create(""); err == nil {
|
||||
t.Fatal("missing language did not generate error")
|
||||
}
|
||||
|
||||
// create a Function Service call witn an unsupported language should bubble
|
||||
// create a Service Function call witn an unsupported language should bubble
|
||||
// the error generated by the underlying initializer.
|
||||
if err := client.Create("cobol"); err == nil {
|
||||
t.Fatal("unsupported language did not generate error")
|
||||
|
@ -134,7 +134,7 @@ func TestCreateDelegates(t *testing.T) {
|
|||
t.Fatalf("builder expected path '%v', got '%v'", expectedPath, path)
|
||||
}
|
||||
// The final image name will be determined by the builder implementation,
|
||||
// but whatever it is (in this case fabricarted); it should be returned
|
||||
// but whatever it is (in this case fabricated); it should be returned
|
||||
// and later provided to the pusher.
|
||||
return image, nil
|
||||
}
|
||||
|
@ -307,6 +307,85 @@ func TestRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestUpdate ensures that the updater properly invokes the build/push/deploy
|
||||
// process, erroring if run on a directory uncreated.
|
||||
func TestUpdate(t *testing.T) {
|
||||
var (
|
||||
path = "testdata/example.com/admin" // .. expected to be initialized
|
||||
name = "admin.example.com" // expected to be derived
|
||||
image = "my.hub/user/admin.exampe.com" // expected image
|
||||
builder = mock.NewBuilder()
|
||||
pusher = mock.NewPusher()
|
||||
updater = mock.NewUpdater()
|
||||
)
|
||||
|
||||
client, err := client.New(
|
||||
client.WithRoot(path), // set function root
|
||||
client.WithBuilder(builder), // builds an image
|
||||
client.WithPusher(pusher), // pushes images to a registry
|
||||
client.WithUpdater(updater), // updates deployed image
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Register function delegates on the mocks which validate assertions
|
||||
// -------------
|
||||
|
||||
// The builder should be invoked with a service name and path to its source
|
||||
// function code. For this test, it is a name derived from the test path.
|
||||
// An example image name is returned.
|
||||
builder.BuildFn = func(name2, path2 string) (string, error) {
|
||||
if name != name {
|
||||
t.Fatalf("builder expected name %v, got '%v'", name, name2)
|
||||
}
|
||||
// The final image name will be determined by the builder implementation,
|
||||
// but whatever it is (in this case fabricated); it should be returned
|
||||
// and later provided to the pusher.
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// The pusher should be invoked with the image to push.
|
||||
pusher.PushFn = func(image2 string) error {
|
||||
if image2 != image {
|
||||
t.Fatalf("pusher expected image '%v', got '%v'", image, image2)
|
||||
}
|
||||
// image of given name wouold be pushed to the configured registry.
|
||||
return nil
|
||||
}
|
||||
|
||||
// The updater should be invoked with the service name and image.
|
||||
// Actual logic of updating is an implementation detail.
|
||||
updater.UpdateFn = func(name2, image2 string) error {
|
||||
if name2 != name {
|
||||
t.Fatalf("deployer expected name '%v', got '%v'", name, name2)
|
||||
}
|
||||
if image2 != image {
|
||||
t.Fatalf("deployer expected image '%v', got '%v'", image, image2)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invocation
|
||||
// -------------
|
||||
|
||||
// Invoke the creation, triggering the function delegates, and
|
||||
// perform follow-up assertions that the functions were indeed invoked.
|
||||
if err := client.Update(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !builder.BuildInvoked {
|
||||
t.Fatal("builder was not invoked")
|
||||
}
|
||||
if !pusher.PushInvoked {
|
||||
t.Fatal("pusher was not invoked")
|
||||
}
|
||||
if !updater.UpdateInvoked {
|
||||
t.Fatal("updater was not invoked")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemove ensures that the remover is invoked with the name of the
|
||||
// client's associated service function.
|
||||
func TestRemove(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package mock
|
||||
|
||||
type Updater struct {
|
||||
UpdateInvoked bool
|
||||
UpdateFn func(name, image string) error
|
||||
}
|
||||
|
||||
func NewUpdater() *Updater {
|
||||
return &Updater{
|
||||
UpdateFn: func(string, string) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Updater) Update(name, image string) error {
|
||||
i.UpdateInvoked = true
|
||||
return i.UpdateFn(name, image)
|
||||
}
|
|
@ -17,7 +17,7 @@ func init() {
|
|||
root.AddCommand(createCmd)
|
||||
|
||||
// register command flags and bind them to environment variables.
|
||||
createCmd.Flags().BoolP("local", "l", false, "create the function service locally only.")
|
||||
createCmd.Flags().BoolP("local", "l", false, "create the service function locally only.")
|
||||
viper.BindPFlag("local", createCmd.Flags().Lookup("local"))
|
||||
|
||||
createCmd.Flags().StringP("name", "n", "", "optionally specify an explicit name for the serive, overriding path-derivation. $FAAS_NAME")
|
||||
|
|
|
@ -3,7 +3,13 @@ package cmd
|
|||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/lkingland/faas/appsody"
|
||||
"github.com/lkingland/faas/client"
|
||||
"github.com/lkingland/faas/docker"
|
||||
"github.com/lkingland/faas/kn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -18,6 +24,39 @@ var updateCmd = &cobra.Command{
|
|||
RunE: update,
|
||||
}
|
||||
|
||||
func update(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("Update not implemented.")
|
||||
func update(cmd *cobra.Command, args []string) (err error) {
|
||||
var (
|
||||
verbose = viper.GetBool("verbose") // Verbose logging
|
||||
registry = viper.GetString("registry") // Registry (ex: docker.io)
|
||||
namespace = viper.GetString("namespace") // namespace at registry (user or org name)
|
||||
)
|
||||
|
||||
// Namespace can not be defaulted.
|
||||
if namespace == "" {
|
||||
return errors.New("image registry namespace (--namespace or FAAS_NAMESPACE is required)")
|
||||
}
|
||||
|
||||
// Builder creates images from function source.
|
||||
builder := appsody.NewBuilder(registry, namespace)
|
||||
builder.Verbose = verbose
|
||||
|
||||
// Pusher of images
|
||||
pusher := docker.NewPusher()
|
||||
pusher.Verbose = verbose
|
||||
|
||||
// Deployer of built images.
|
||||
updater := kn.NewUpdater()
|
||||
updater.Verbose = verbose
|
||||
|
||||
client, err := client.New(
|
||||
client.WithVerbose(verbose),
|
||||
client.WithBuilder(builder),
|
||||
client.WithPusher(pusher),
|
||||
client.WithUpdater(updater),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return client.Update()
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
// Version
|
||||
// Printed on subcommand `version` or flag `--version`
|
||||
const Version = "v0.0.8"
|
||||
const Version = "v0.0.12"
|
||||
|
||||
func init() {
|
||||
root.AddCommand(versionCmd)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package kn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/lkingland/faas/k8s"
|
||||
)
|
||||
|
||||
// Updater implemented using the kn binary.
|
||||
type Updater struct {
|
||||
// Verbose logging.
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// NewUpdater creates an instance of the kubectl-based deployer.
|
||||
func NewUpdater() *Updater {
|
||||
return &Updater{}
|
||||
}
|
||||
|
||||
// Update the named service with the new image.
|
||||
func (d *Updater) Update(name, image string) (err error) {
|
||||
// assert kubectl
|
||||
if _, err = exec.LookPath("kn"); err != nil {
|
||||
return errors.New("please install 'kn'")
|
||||
}
|
||||
|
||||
// Convert the project name proper (a valid domain) to how it is being
|
||||
// represented in kubernetes: as a domain label (RFC1035)
|
||||
// for use as the service's deployed name.
|
||||
project, err := k8s.ToSubdomain(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := fmt.Sprintf("BUILT=%v", time.Now().Format("20060102T150405"))
|
||||
|
||||
// TODO: use knative client directly.
|
||||
// TODO: use tags and traffic splitting.
|
||||
cmd := exec.Command("kn", "service", "update", project, "--env", timestamp)
|
||||
|
||||
// If verbose logging is enabled, echo appsody's chatty stdout.
|
||||
if d.Verbose {
|
||||
fmt.Println(cmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
// Capture stderr for echoing on failure.
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run the command, echoing captured stderr as well ass the cmd internal error.
|
||||
if err = cmd.Run(); err != nil {
|
||||
// TODO: sanitize stderr from appsody, or submit a PR to remove duplicates etc.
|
||||
return errors.New(fmt.Sprintf("%v. %v", string(stderr.Bytes()), err.Error()))
|
||||
}
|
||||
|
||||
// TODO: explicitly pull address:
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue