224 lines
6.3 KiB
Go
224 lines
6.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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)
|