vaguerent/internal/core/operation.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)