internal/command/command.go (108 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package command facilitates calling commands within the guest-agent.
package command
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
"github.com/GoogleCloudPlatform/galog"
)
// KnownListeners is the set of known pipes that can be used to send commands.
type KnownListeners int
const (
// ListenerGuestAgent pipe sends command to a running Guest Agent.
ListenerGuestAgent KnownListeners = iota
// ListenerCorePlugin pipe sends command to a running core plugin.
ListenerCorePlugin
)
// String returns the string representation of the KnownListeners enum which
// help print enum fields as string when used with `%v` for example instead of
// numbers. Additionally, same string representation is used as identifiers.
func (l KnownListeners) String() string {
switch l {
case ListenerGuestAgent:
return "guestagent"
case ListenerCorePlugin:
return "coreplugin"
default:
return fmt.Sprintf("%d", l)
}
}
// CurrentMonitor returns the current command monitor which can be used to
// register command handlers.
func CurrentMonitor() *Monitor {
return cmdMonitor
}
// Handler functions are the business logic of commands. They must process json
// encoded as a byte slice which contains a Command field and optional arbitrary
// data, and return json which contains a Status, StatusMessage, and optional
// arbitrary data (again encoded as a byte slice). Returned errors will be
// passed onto the command requester.
type Handler func(context.Context, []byte) ([]byte, error)
// Request is the basic request structure. Command determines which handler the
// request is routed to. Callers may set additional arbitrary fields.
type Request struct {
Command string
}
// Response is the basic response structure. Handlers may set additional
// arbitrary fields.
type Response struct {
// Status code for the request. Meaning is defined by the caller, but
// conventionally zero is success.
Status int
// StatusMessage is an optional message defined by the caller. Should
// generally help a human understand what happened.
StatusMessage string
}
var (
// CmdNotFoundError is return when there is no handler for the request
// command.
CmdNotFoundError = Response{
Status: 101,
StatusMessage: "Could not find a handler for the requested command",
}
// BadRequestError is returned for invalid or unparseable JSON
BadRequestError = Response{
Status: 102,
StatusMessage: "Could not parse valid JSON from request",
}
// ConnError is returned for errors from the underlying communication
// protocol.
ConnError = Response{
Status: 103,
StatusMessage: "Connection error",
}
// TimeoutError is returned when the timeout period elapses before valid JSON
// is received.
TimeoutError = Response{
Status: 104,
StatusMessage: "Connection timeout before reading valid request",
}
// HandlerError is returned when the handler function returns an non-nil
// error. The status message will be replaced with the returned error string.
HandlerError = Response{
Status: 105,
StatusMessage: "The command handler encountered an error processing your request",
}
internalError = []byte(`{"Status":106,"StatusMessage":"The command server encountered an internal error trying to respond to your request"}`)
)
// RegisterHandler registers f as the handler for cmd. If a command.Server has
// been initialized, it will be signalled to start listening for commands.
func (m *Monitor) RegisterHandler(cmd string, f Handler) error {
m.handlersMu.Lock()
defer m.handlersMu.Unlock()
if _, ok := m.handlers[cmd]; ok {
return fmt.Errorf("cmd %s is already handled", cmd)
}
m.handlers[cmd] = f
return nil
}
// UnregisterHandler clears the handlers for cmd. If a command.Server has been
// initialized and there are no more handlers registered, the server will be
// signalled to stop listening for commands.
func (m *Monitor) UnregisterHandler(cmd string) error {
m.handlersMu.Lock()
defer m.handlersMu.Unlock()
if _, ok := m.handlers[cmd]; !ok {
return fmt.Errorf("cmd %s is not registered", cmd)
}
delete(m.handlers, cmd)
return nil
}
// SendCommand sends a command request over the configured pipe.
func SendCommand(ctx context.Context, req []byte, listener KnownListeners) []byte {
return sendCmdPipe(ctx, PipeName(listener), req)
}
// sendCmdPipe sends a command request over a specific pipe.
func sendCmdPipe(ctx context.Context, pipe string, req []byte) []byte {
conn, err := dialPipe(ctx, pipe)
if err != nil {
galog.Errorf("Failed to dial in %q with error: %v", pipe, err)
if b, err := json.Marshal(ConnError); err == nil {
return b
}
return internalError
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(parseTimeoutFromCfg()))
i, err := conn.Write(req)
if err != nil || i != len(req) {
galog.Errorf("Sending command on pipe failed error: %v, bytes wrote: %d, expected : %d", err, i, len(req))
if b, err := json.Marshal(ConnError); err == nil {
return b
}
return internalError
}
data, err := io.ReadAll(conn)
if err != nil {
galog.Errorf("Failed to read data from pipe with error: %v", err)
if b, err := json.Marshal(ConnError); err == nil {
return b
}
return internalError
}
return data
}