github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/payload/context/context.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package context
     5  
     6  import (
     7  	"sort"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  
    12  	"github.com/juju/juju/payload"
    13  )
    14  
    15  var logger = loggo.GetLogger("juju.payload.context")
    16  
    17  // APIClient represents the API needs of a Context.
    18  type APIClient interface {
    19  	// List requests the payload info for the given IDs.
    20  	List(fullIDs ...string) ([]payload.Result, error)
    21  	// Track sends a request to update state with the provided payloads.
    22  	Track(payloads ...payload.Payload) ([]payload.Result, error)
    23  	// Untrack removes the payloads from our list track.
    24  	Untrack(fullIDs ...string) ([]payload.Result, error)
    25  	// SetStatus sets the status for the given IDs.
    26  	SetStatus(status string, fullIDs ...string) ([]payload.Result, error)
    27  }
    28  
    29  // TODO(ericsnow) Rename Get and Set to more specifically describe what
    30  // they are for.
    31  
    32  // Component provides the hook context data specific to payloads.
    33  type Component interface {
    34  	// Get returns the payload info corresponding to the given ID.
    35  	Get(class, id string) (*payload.Payload, error)
    36  	// Track records the payload info in the hook context.
    37  	Track(payload payload.Payload) error
    38  	// Untrack removes the payload from our list of payloads to track.
    39  	Untrack(class, id string) error
    40  	// SetStatus sets the status of the payload.
    41  	SetStatus(class, id, status string) error
    42  	// List returns the list of registered payload IDs.
    43  	List() ([]string, error)
    44  	// Flush pushes the hook context data out to state.
    45  	Flush() error
    46  }
    47  
    48  var _ Component = (*Context)(nil)
    49  
    50  // Context is the payload portion of the hook context.
    51  type Context struct {
    52  	api     APIClient
    53  	dataDir string
    54  	// TODO(ericsnow) Use the Juju ID for the key rather than Info.ID().
    55  	payloads map[string]payload.Payload
    56  	updates  map[string]payload.Payload
    57  }
    58  
    59  // NewContext returns a new jujuc.ContextComponent for payloads.
    60  func NewContext(api APIClient, dataDir string) *Context {
    61  	return &Context{
    62  		api:      api,
    63  		dataDir:  dataDir,
    64  		payloads: make(map[string]payload.Payload),
    65  		updates:  make(map[string]payload.Payload),
    66  	}
    67  }
    68  
    69  // NewContextAPI returns a new jujuc.ContextComponent for payloads.
    70  func NewContextAPI(api APIClient, dataDir string) (*Context, error) {
    71  	results, err := api.List()
    72  	if err != nil {
    73  		return nil, errors.Trace(err)
    74  	}
    75  
    76  	ctx := NewContext(api, dataDir)
    77  	for _, result := range results {
    78  		pl := result.Payload
    79  		// TODO(ericsnow) Use id instead of pl.FullID().
    80  		ctx.payloads[pl.FullID()] = pl.Payload
    81  	}
    82  	return ctx, nil
    83  }
    84  
    85  // HookContext is the portion of jujuc.Context used in this package.
    86  type HookContext interface {
    87  	// Component implements jujuc.Context.
    88  	Component(string) (Component, error)
    89  }
    90  
    91  // ContextComponent returns the hook context for the payload
    92  // payload component.
    93  func ContextComponent(ctx HookContext) (Component, error) {
    94  	compCtx, err := ctx.Component(payload.ComponentName)
    95  	if errors.IsNotFound(err) {
    96  		return nil, errors.Errorf("component %q not registered", payload.ComponentName)
    97  	}
    98  	if err != nil {
    99  		return nil, errors.Trace(err)
   100  	}
   101  	if compCtx == nil {
   102  		return nil, errors.Errorf("component %q disabled", payload.ComponentName)
   103  	}
   104  	return compCtx, nil
   105  }
   106  
   107  // TODO(ericsnow) Should we build in refreshes in all the methods?
   108  
   109  // Payloads returns the payloads known to the context.
   110  func (c *Context) Payloads() ([]payload.Payload, error) {
   111  	payloads := mergePayloadMaps(c.payloads, c.updates)
   112  	var newPayloads []payload.Payload
   113  	for _, pl := range payloads {
   114  		newPayloads = append(newPayloads, pl)
   115  	}
   116  
   117  	return newPayloads, nil
   118  }
   119  
   120  func mergePayloadMaps(payloads, updates map[string]payload.Payload) map[string]payload.Payload {
   121  	// At this point payloads and updates have already been checked for
   122  	// nil values so we won't see any here.
   123  	result := make(map[string]payload.Payload)
   124  	for k, v := range payloads {
   125  		result[k] = v
   126  	}
   127  	for k, v := range updates {
   128  		result[k] = v
   129  	}
   130  	return result
   131  }
   132  
   133  // Get returns the payload info corresponding to the given ID.
   134  func (c *Context) Get(class, id string) (*payload.Payload, error) {
   135  	fullID := payload.BuildID(class, id)
   136  	logger.Tracef("getting %q from hook context", fullID)
   137  
   138  	actual, ok := c.updates[fullID]
   139  	if !ok {
   140  		actual, ok = c.payloads[fullID]
   141  		if !ok {
   142  			return nil, errors.NotFoundf("%s", fullID)
   143  		}
   144  	}
   145  	return &actual, nil
   146  }
   147  
   148  // List returns the sorted names of all registered payloads.
   149  func (c *Context) List() ([]string, error) {
   150  	logger.Tracef("listing all payloads in hook context")
   151  
   152  	payloads, err := c.Payloads()
   153  	if err != nil {
   154  		return nil, errors.Trace(err)
   155  	}
   156  	if len(payloads) == 0 {
   157  		return nil, nil
   158  	}
   159  	var ids []string
   160  	for _, wl := range payloads {
   161  		ids = append(ids, wl.FullID())
   162  	}
   163  	sort.Strings(ids)
   164  	return ids, nil
   165  }
   166  
   167  // Track records the payload info in the hook context.
   168  func (c *Context) Track(pl payload.Payload) error {
   169  	logger.Tracef("adding %q to hook context: %#v", pl.FullID(), pl)
   170  
   171  	if err := pl.Validate(); err != nil {
   172  		return errors.Trace(err)
   173  	}
   174  
   175  	// TODO(ericsnow) We are likely missing mechanisim for local persistence.
   176  	id := pl.FullID()
   177  	c.updates[id] = pl
   178  	return nil
   179  }
   180  
   181  // Untrack tells juju to stop tracking this payload.
   182  func (c *Context) Untrack(class, id string) error {
   183  	fullID := payload.BuildID(class, id)
   184  	logger.Tracef("Calling untrack on payload context %q", fullID)
   185  
   186  	res, err := c.api.Untrack(fullID)
   187  	if err != nil {
   188  		return errors.Trace(err)
   189  	}
   190  	// TODO(ericsnow) We should not ignore a 0-len result.
   191  	if len(res) > 0 && res[0].Error != nil {
   192  		return errors.Trace(res[0].Error)
   193  	}
   194  	delete(c.payloads, id)
   195  
   196  	return nil
   197  }
   198  
   199  // SetStatus sets the identified payload's status.
   200  func (c *Context) SetStatus(class, id, status string) error {
   201  	fullID := payload.BuildID(class, id)
   202  	logger.Tracef("Calling status-set on payload context %q", fullID)
   203  
   204  	res, err := c.api.SetStatus(status, fullID)
   205  	if err != nil {
   206  		return errors.Trace(err)
   207  	}
   208  	// TODO(ericsnow) We should not ignore a 0-len result.
   209  	if len(res) > 0 && res[0].Error != nil {
   210  		// In a hook context, the case where the specified payload does
   211  		// not exist is a special one. A hook tool is how a charm author
   212  		// communicates the state of the charm. So returning an error
   213  		// here in the "missing" case makes less sense than in other
   214  		// places. We could simply ignore any error that surfaces for
   215  		// that case. However, returning the error communicates to the
   216  		// charm author that what they're trying to communicate doesn't
   217  		// make sense.
   218  		return errors.Trace(res[0].Error)
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // TODO(ericsnow) The context machinery is not actually using this yet.
   225  
   226  // Flush implements jujuc.ContextComponent. In this case that means all
   227  // added and updated payload.Payload in the hook context are pushed to
   228  // Juju state via the API.
   229  func (c *Context) Flush() error {
   230  	logger.Tracef("flushing from hook context to state")
   231  	// TODO(natefinch): make this a noop and move this code into set.
   232  
   233  	if len(c.updates) > 0 {
   234  		var updates []payload.Payload
   235  		for _, pl := range c.updates {
   236  			updates = append(updates, pl)
   237  		}
   238  
   239  		res, err := c.api.Track(updates...)
   240  		if err != nil {
   241  			return errors.Trace(err)
   242  		}
   243  		if len(res) > 0 && res[0].Error != nil {
   244  			return errors.Trace(res[0].Error)
   245  		}
   246  
   247  		for k, v := range c.updates {
   248  			c.payloads[k] = v
   249  		}
   250  		c.updates = map[string]payload.Payload{}
   251  	}
   252  	return nil
   253  }