forked from knative/func
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
577 lines (483 loc) · 16.2 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
package faas
import (
"errors"
"fmt"
"io"
"os"
)
const (
DefaultNamespace = "faas"
DefaultRegistry = "docker.io"
DefaultRuntime = "go"
DefaultTrigger = "http"
DefaultMaxRecursion = 5 // when determining a name from path
)
// Client for managing Function instances.
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
runner Runner // Runs the Function locally
remover Remover // Removes remote services
lister Lister // Lists remote services
describer Describer
dnsProvider DNSProvider // Provider of DNS services
templates string // path to extensible templates
repository string // default repo for OCI image tags
domainSearchLimit int // max recursion when deriving domain
progressListener ProgressListener // progress listener
}
// Builder of Function source to runnable image.
type Builder interface {
// Build a Function project with source located at path.
Build(Function) error
}
// Pusher of Function image to a registry.
type Pusher interface {
// Push the image of the Function.
Push(Function) error
}
// Deployer of Function source to running status.
type Deployer interface {
// Deploy a Function of given name, using given backing image.
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.
Run(Function) error
}
// Remover of deployed services.
type Remover interface {
// Remove the Function from remote.
Remove(name string) error
}
// Lister of deployed services.
type Lister interface {
// List the Functions currently deployed.
List() ([]string, error)
}
// 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)
// 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 Functions' remote deployed aspect.
type Describer interface {
// Describe the running state of the service as reported by the underlyng platform.
Describe(name string) (description Description, err error)
}
type Description struct {
Name string `json:"name" yaml:"name"`
Routes []string `json:"routes" yaml:"routes"`
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
}
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
}
// 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},
updater: &noopUpdater{output: os.Stdout},
runner: &noopRunner{output: os.Stdout},
remover: &noopRemover{output: os.Stdout},
lister: &noopLister{output: os.Stdout},
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &noopProgressListener{},
domainSearchLimit: DefaultMaxRecursion, // no recursion limit deriving domain by default.
}
// Apply passed options, which take ultimate precidence.
for _, o := range options {
o(c)
}
return c
}
// 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
}
}
// 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) {
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
}
}
// WithDomainSearchLimit sets the maximum levels of upward recursion used when
// attempting to derive effective DNS name from root path. Ignored if DNS was
// explicitly set via WithName.
func WithDomainSearchLimit(limit int) Option {
return func(c *Client) {
c.domainSearchLimit = limit
}
}
// WithTemplates sets the location to use for extensible templates.
// Extensible templates are additional templates that exist on disk and are
// not built into the binary.
func WithTemplates(templates string) Option {
return func(c *Client) {
c.templates = templates
}
}
// WithRepository 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 WithRepository(repository string) Option {
return func(c *Client) {
c.repository = repository
}
}
// Create a Function.
// Includes Initialization, Building, and Deploying.
func (c *Client) Create(cfg Function) (err error) {
c.progressListener.SetTotal(4)
defer c.progressListener.Done()
// Initialize, writing out a template implementation and a config file.
// TODO: the Function's Initialize parameters are slightly different than
// the Initializer interface, and can thus cause confusion (one passes an
// optional name the other passes root path). This could easily cause
// confusion and thus we may want to rename Initalizer to the more specific
// task it performs: ContextTemplateWriter or similar.
c.progressListener.Increment("Initializing new Function project")
err = c.Initialize(cfg)
if 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(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(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("Create complete")
// 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
}
// Initialize creates a new Function project locally using the settings
// provided on a Function object.
func (c *Client) Initialize(cfg Function) (err error) {
// Create Function of the given root path.
f, err := NewFunction(cfg.Root)
if err != nil {
return
}
// Assert the specified root is free of visible files and contentious
// hidden files (the ConfigFile, which indicates it is already initialized)
if err = assertEmptyRoot(f.Root); err != nil {
return
}
// Set the name to that provided, defaulting to path derivation if empty.
f.Name = cfg.Name
if cfg.Name == "" {
f.Name = pathToDomain(f.Root, c.domainSearchLimit)
if f.Name == "" {
err = errors.New("Function name must be deriveable from path or explicitly provided")
return
}
}
// Assert runtime was provided, or default.
f.Runtime = cfg.Runtime
if f.Runtime == "" {
f.Runtime = DefaultRuntime
}
// Assert trigger was provided, or default.
f.Trigger = cfg.Trigger
if f.Trigger == "" {
f.Trigger = DefaultTrigger
}
// Write out a template.
w := templateWriter{templates: c.templates, verbose: c.verbose}
if err = w.Write(f.Runtime, f.Trigger, f.Root); err != nil {
return
}
// Write out the config.
if err = writeConfig(f); 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("OK %v %v\n", f.Name, f.Root)
}
return
}
// Build the Function at path. Errors if the Function is either unloadable or does
// not contain a populated Image.
func (c *Client) Build(path string) (err error) {
f, err := NewFunction(path)
if err != nil {
return
}
// Derive Image from the path (preceidence is given to extant config)
if f.Image, err = DerivedImage(path, c.repository); err != nil {
return
}
if err = c.builder.Build(f); err != nil {
return
}
// Write out config, which will now contain a populated image tag
// if it had not already
if err = writeConfig(f); err != nil {
return
}
// TODO: create a statu structure and return it here for optional
// use by the cli for user echo (rather than rely on verbose mode here)
if c.verbose {
fmt.Printf("OK %v\n", f.Image)
}
return
}
// 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 {
return
}
if err = c.deployer.Deploy(f); err != nil {
return
}
if c.verbose {
// TODO: aspirational. Should be an actual route echo.
fmt.Printf("OK https://%v/\n", f.Image)
}
return
}
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)
}
// 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 {
// Create an instance of a Function representation at the given root.
f, err := NewFunction(root)
if err != nil {
return err
}
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 in order to run.", root)
}
// delegate to concrete implementation of runner entirely.
return c.runner.Run(f)
}
// List currently deployed Functions.
func (c *Client) List() ([]string, error) {
// delegate to concrete implementation of lister entirely.
return c.lister.List()
}
// Describe a Function. Name takes precidence. If no name is provided,
// the Function defined at root is used.
func (c *Client) Describe(name, root string) (d Description, err error) {
// If name is provided, it takes precidence.
// Otherwise load the Function defined at root.
if name != "" {
return c.describer.Describe(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(f.Name)
}
// 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(cfg Function) error {
// If name is provided, it takes precidence.
// Otherwise load the Function deined at root.
if cfg.Name != "" {
return c.remover.Remove(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)
}
return c.remover.Remove(f.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
// development, however, it is usefule that they are defaulted to noops and
// provded only when necessary. Unit tests for the concrete implementations
// serve to keep the core logic here separate from the imperitive.
// -----------------------------------------------------
type noopBuilder struct{ output io.Writer }
func (n *noopBuilder) Build(_ Function) error { return nil }
type noopPusher struct{ output io.Writer }
func (n *noopPusher) Push(_ Function) error { return nil }
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 }
type noopRemover struct{ output io.Writer }
func (n *noopRemover) Remove(string) error { return nil }
type noopLister struct{ output io.Writer }
func (n *noopLister) List() ([]string, error) { return []string{}, nil }
type noopDNSProvider struct{ output io.Writer }
func (n *noopDNSProvider) Provide(_ Function) error { return nil }
type noopProgressListener struct{}
func (p *noopProgressListener) SetTotal(i int) {}
func (p *noopProgressListener) Increment(m string) {}
func (p *noopProgressListener) Complete(m string) {}
func (p *noopProgressListener) Done() {}