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
|
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package hookstate
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/snapcore/snapd/jsonutil"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/randutil"
"github.com/snapcore/snapd/snap"
)
// Context represents the context under which the snap is calling back into snapd.
// It is associated with a task when the callback is happening from within a hook,
// or otherwise considered an ephemeral context in that its associated data will
// be discarded once that individual call is finished.
type Context struct {
task *state.Task
state *state.State
setup *HookSetup
id string
handler Handler
cache map[any]any
onDone []func() error
mutex sync.Mutex
mutexChecker int32
}
// NewContext returns a new context associated with the provided task or
// an ephemeral context if task is nil.
//
// A random ID is generated if contextID is empty.
func NewContext(task *state.Task, state *state.State, setup *HookSetup, handler Handler, contextID string) (*Context, error) {
if contextID == "" {
var err error
contextID, err = randutil.CryptoToken(32)
if err != nil {
return nil, err
}
}
return &Context{
task: task,
state: state,
setup: setup,
id: contextID,
handler: handler,
cache: make(map[any]any),
}, nil
}
func newEphemeralHookContextWithData(st *state.State, setup *HookSetup, contextData map[string]any) (*Context, error) {
context, err := NewContext(nil, st, setup, nil, "")
if err != nil {
return nil, err
}
if contextData != nil {
serialized, err := json.Marshal(contextData)
if err != nil {
return nil, err
}
var data map[string]*json.RawMessage
if err := json.Unmarshal(serialized, &data); err != nil {
return nil, err
}
context.cache["ephemeral-context"] = data
}
return context, nil
}
// InstanceName returns the name of the snap instance containing the hook.
func (c *Context) InstanceName() string {
return c.setup.Snap
}
// HookSource returns a string that identifies the source of a hook. This could
// either be a snap or a component. Snaps will be in the form "<snap_instance>".
// Components will be in the form "<snap_instance>+<component_name>".
func (c *Context) HookSource() string {
if c.setup.Component == "" {
return c.setup.Snap
}
return snap.SnapComponentName(c.setup.Snap, c.setup.Component)
}
// IsComponentHook returns true if this context is associated with a component
// hook.
func (c *Context) IsComponentHook() bool {
return !c.IsSnapHook()
}
// IsSnapHook returns true if this context is associated with a snap hook.
func (c *Context) IsSnapHook() bool {
return c.setup.Component == ""
}
// ComponentName returns the name of the component containing the hook. If the
// hook is not associated with a component, it returns an empty string.
func (c *Context) ComponentName() string {
return c.setup.Component
}
// SnapRevision returns the revision of the snap containing the hook.
func (c *Context) SnapRevision() snap.Revision {
return c.setup.Revision
}
// ComponentRevision returns the revision of the snap component containing the
// hook. This returned revision is only valid if the hook is a component hook.
func (c *Context) ComponentRevision() snap.Revision {
return c.setup.ComponentRevision
}
// Task returns the task associated with the hook or (nil, false) if the context is ephemeral
// and task is not available.
func (c *Context) Task() (*state.Task, bool) {
return c.task, c.task != nil
}
// HookName returns the name of the hook in this context.
func (c *Context) HookName() string {
return c.setup.Hook
}
// Timeout returns the maximum time this hook can run
func (c *Context) Timeout() time.Duration {
return c.setup.Timeout
}
// ID returns the ID of the context.
func (c *Context) ID() string {
return c.id
}
// Handler returns the handler for this context
func (c *Context) Handler() Handler {
return c.handler
}
// Lock acquires the lock for this context (required for Set/Get, Cache/Cached, Logf/Errorf),
// and OnDone/Done).
func (c *Context) Lock() {
c.mutex.Lock()
c.state.Lock()
atomic.AddInt32(&c.mutexChecker, 1)
}
// Unlock releases the lock for this context.
func (c *Context) Unlock() {
atomic.AddInt32(&c.mutexChecker, -1)
c.state.Unlock()
c.mutex.Unlock()
}
func (c *Context) reading() {
if atomic.LoadInt32(&c.mutexChecker) != 1 {
panic("internal error: accessing context without lock")
}
}
func (c *Context) writing() {
if atomic.LoadInt32(&c.mutexChecker) != 1 {
panic("internal error: accessing context without lock")
}
}
// Set associates value with key. The provided value must properly marshal and
// unmarshal with encoding/json. Note that the context needs to be locked and
// unlocked by the caller.
func (c *Context) Set(key string, value any) {
c.writing()
var data map[string]*json.RawMessage
if c.IsEphemeral() {
data, _ = c.cache["ephemeral-context"].(map[string]*json.RawMessage)
} else {
if err := c.task.Get("hook-context", &data); err != nil && !errors.Is(err, state.ErrNoState) {
panic(fmt.Sprintf("internal error: cannot unmarshal context: %v", err))
}
}
if data == nil {
data = make(map[string]*json.RawMessage)
}
marshalledValue, err := json.Marshal(value)
if err != nil {
panic(fmt.Sprintf("internal error: cannot marshal context value for %q: %s", key, err))
}
raw := json.RawMessage(marshalledValue)
data[key] = &raw
if c.IsEphemeral() {
c.cache["ephemeral-context"] = data
} else {
c.task.Set("hook-context", data)
}
}
// Get unmarshals the stored value associated with the provided key into the
// value parameter. Note that the context needs to be locked/unlocked by the
// caller.
func (c *Context) Get(key string, value any) error {
c.reading()
var data map[string]*json.RawMessage
if c.IsEphemeral() {
data, _ = c.cache["ephemeral-context"].(map[string]*json.RawMessage)
if data == nil {
return state.ErrNoState
}
} else {
if err := c.task.Get("hook-context", &data); err != nil {
return err
}
}
raw, ok := data[key]
if !ok {
return state.ErrNoState
}
err := jsonutil.DecodeWithNumber(bytes.NewReader(*raw), &value)
if err != nil {
return fmt.Errorf("cannot unmarshal context value for %q: %s", key, err)
}
return nil
}
// State returns the state contained within the context
func (c *Context) State() *state.State {
return c.state
}
// Cached returns the cached value associated with the provided key. It returns
// nil if there is no entry for key. Note that the context needs to be locked
// and unlocked by the caller.
func (c *Context) Cached(key any) any {
c.reading()
return c.cache[key]
}
// Cache associates value with key. The cached value is not persisted. Note that
// the context needs to be locked/unlocked by the caller.
func (c *Context) Cache(key, value any) {
c.writing()
c.cache[key] = value
}
// OnDone requests the provided function to be run once the context knows it's
// complete. This can be called multiple times; each function will be called in
// the order in which they were added. Note that the context needs to be locked
// and unlocked by the caller.
func (c *Context) OnDone(f func() error) {
c.writing()
c.onDone = append(c.onDone, f)
}
// Done is called to notify the context that its hook has exited successfully.
// It will call all of the functions added in OnDone (even if one of them
// returns an error) and will return the first error encountered. Note that the
// context needs to be locked/unlocked by the caller.
func (c *Context) Done() error {
c.reading()
var firstErr error
for _, f := range c.onDone {
if err := f(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (c *Context) IsEphemeral() bool {
return c.task == nil
}
// ChangeID returns change ID for non-ephemeral context
// or empty string otherwise.
func (c *Context) ChangeID() string {
if task, ok := c.Task(); ok {
if chg := task.Change(); chg != nil {
return chg.ID()
}
}
return ""
}
// Logf logs to the context, either to the logger for ephemeral contexts
// or the task log.
//
// Context must be locked.
func (c *Context) Logf(fmt string, args ...any) {
c.writing()
if c.IsEphemeral() {
logger.Noticef(fmt, args...)
} else {
c.task.Logf(fmt, args...)
}
}
// Errorf logs errors to the context, either to the logger for
// ephemeral contexts or the task log.
//
// Context must be locked.
func (c *Context) Errorf(format string, args ...any) {
c.writing()
if c.IsEphemeral() {
// XXX: loger has no Errorf() :/
logger.Noticef(format, args...)
} else {
c.task.Errorf(format, args...)
// If errors are ignored the task will not be in "Error"
// state so the error is hard to find. In this case also
// log errors to the journal to ensure that e.g. seeding
// configure errors are observable.
if c.setup != nil && c.setup.IgnoreError {
msg := fmt.Sprintf(format, args...)
logger.Noticef("ERROR task %v (%v): %v", c.task.ID(), c.task.Summary(), msg)
}
}
}
|