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
|
package core
import (
"context"
"fmt"
"reflect"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/pkg/finalcontext"
"github.com/hashicorp/vagrant/internal/server"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
type scope interface {
UI() (terminal.UI, error)
Ref() interface{}
JobInfo() *component.JobInfo
Client() *serverclient.VagrantClient
execHook(ctx context.Context, log hclog.Logger, h *config.Hook) (err error)
}
// operation is a private interface that we implement for "operations" such
// as build, deploy, push, etc. This lets us share logic around creating
// server metadata, error checking, etc.
type operation interface {
// Init returns a new metadata message we'll upsert
Init(scope) (proto.Message, error)
// Upsert performs an upsert operation for some metadata
Upsert(context.Context, vagrant_server.VagrantClient, proto.Message) (proto.Message, error)
// Do performs the actual operation and returns the result that you
// want to return from the operation. This result will be marshaled into
// the ValuePtr if it implements ProtoMarshaler.
// Do can alter the proto.Message into it's final form, as it's the value
// returned by Init and that will be written back via Upsert after Do
// has completed.
Do(context.Context, hclog.Logger, scope, proto.Message) (interface{}, error)
// StatusPtr and ValuePtr return pointers to the fields in the message
// for the status and values respectively.
StatusPtr(proto.Message) **vagrant_server.Status
ValuePtr(proto.Message) **anypb.Any
// Hooks are the hooks to execute as part of this operation keyed by "when"
Hooks(scope) map[string][]*config.Hook
// Labels is called to return any labels that should be set for this
// operation. This should include the component labels. These will be merged
// with any resulting labels from the operation.
Labels(scope) map[string]string
}
func doOperation(
ctx context.Context,
log hclog.Logger,
s scope,
op operation,
) (interface{}, proto.Message, error) {
// Get our hooks
hooks := op.Hooks(s)
// Init the metadata
msg, err := op.Init(s)
if err != nil {
return nil, nil, err
}
// Setup our job id if we have that field.
if f := msgField(msg, "JobId"); f.IsValid() {
f.Set(reflect.ValueOf(s.JobInfo().Id))
}
// If we have no status pointer, then we just allocate one for this
// function. We don't send this anywhere but this just lets us follow
// the remaining logic without a bunch of nil checks.
statusPtr := op.StatusPtr(msg)
if statusPtr == nil {
var status *vagrant_server.Status
statusPtr = &status
}
*statusPtr = server.NewStatus(vagrant_server.Status_RUNNING)
// Upsert the metadata for our running state
log.Debug("creating metadata on server")
msg, err = op.Upsert(ctx, s.Client(), msg)
if err != nil {
return nil, nil, err
}
if id := msgId(msg); id != "" {
log = log.With("id", id)
}
// Reset the status pointer because we might have a new message type
if ptr := op.StatusPtr(msg); ptr != nil {
statusPtr = ptr
}
// Get where we'll set the value. Similar to statusPtr, we set this
// to a local value if we get nil so that we can avoid nil checks.
valuePtr := op.ValuePtr(msg)
if valuePtr == nil {
var value *anypb.Any
valuePtr = &value
}
var doErr error
// If we have before hooks, run those
for i, h := range hooks["before"] {
if err := s.execHook(ctx, log.Named(fmt.Sprintf("hook-before-%d", i)), h); err != nil {
doErr = fmt.Errorf("Error running before hook index %d: %w", i, err)
log.Warn("error running before hook", "err", err)
if h.ContinueOnFailure() {
log.Info("hook configured to continueon failure, ignoring error")
doErr = nil
}
}
}
// Run the actual implementation
var result interface{}
if doErr == nil {
log.Debug("running local operation")
result, doErr = op.Do(ctx, log, s, msg)
if doErr == nil {
// No error, our state is success
server.StatusSetSuccess(*statusPtr)
// Set our final value if we have a value pointer
*valuePtr = nil
if result != nil {
*valuePtr, err = component.ProtoAny(result)
if err != nil {
doErr = err
}
}
}
}
// Run after hooks
if doErr == nil {
for i, h := range hooks["after"] {
if err := s.execHook(ctx, log.Named(fmt.Sprintf("hook-after-%d", i)), h); err != nil {
doErr = fmt.Errorf("Error running after hook index %d: %w", i, err)
log.Warn("error running after hook", "err", err)
if h.ContinueOnFailure() {
log.Info("hook configured to continueon failure, ignoring error")
doErr = nil
}
}
}
}
// If we have an error, then we set the error status
if doErr != nil {
log.Warn("error during local operation", "err", doErr)
*valuePtr = nil
server.StatusSetError(*statusPtr, doErr)
}
// If our context ended we need to create a final context so we
// can attempt to finalize our metadata.
if ctx.Err() != nil {
var cancel context.CancelFunc
ctx, cancel = finalcontext.Context(log)
defer cancel()
}
// Set the final metadata
msg, err = op.Upsert(ctx, s.Client(), msg)
if err != nil {
log.Warn("error marking server metadata as complete", "err", err)
} else {
log.Debug("metadata marked as complete")
}
// If we had an original error, return it now that we have saved all metadata
if doErr != nil {
return nil, nil, doErr
}
return result, msg, nil
}
// msgId gets the id of the message by looking for the "Id" field. This
// will return empty string if the ID field can't be found for any reason.
func msgId(msg proto.Message) string {
val := msgField(msg, "Id")
if !val.IsValid() || val.Kind() != reflect.String {
return ""
}
return val.String()
}
// msgField gets the field from the given message. This will return an
// invalid value if it doesn't exist.
func msgField(msg proto.Message, f string) reflect.Value {
val := reflect.ValueOf(msg)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// Get the Id field
return val.FieldByName(f)
}
var _ scope = (*Basis)(nil)
var _ scope = (*Project)(nil)
var _ scope = (*Target)(nil)
|