Skip to content

Commit

Permalink
Add a generic command (#133)
Browse files Browse the repository at this point in the history
* Add a freeform command resource

* Typo

* Implement a command component

* Regen SDKs
  • Loading branch information
UnstoppableMango authored Aug 14, 2024
1 parent 9acb8f2 commit a7835aa
Show file tree
Hide file tree
Showing 23 changed files with 1,392 additions and 244 deletions.
624 changes: 385 additions & 239 deletions gen/go/unmango/baremetal/v1alpha1/command.pb.go

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions gen/go/unmango/baremetal/v1alpha1/command_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions proto/unmango/baremetal/v1alpha1/command.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ syntax = "proto3";
package unmango.baremetal.v1alpha1;

service CommandService {
rpc Exec(ExecRequest) returns (ExecResponse);
rpc Create(CreateRequest) returns (CreateResponse);
rpc Update(UpdateRequest) returns (UpdateResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
}

message ExecRequest {
repeated string args = 1;
optional string stdin = 2;
}

message ExecResponse {
Result result = 1;
}

message CreateRequest {
Command command = 1;
repeated string expect_created = 2;
Expand Down
1 change: 1 addition & 0 deletions provider/cmd/provisioner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var rootCmd = &cobra.Command{
p.WithLogger(log),
p.WithOptionalCertificates(clientCaFile, certFile, keyFile),
p.WithReflection(reflection),
p.WithWhitelist(whitelist),
)

log.Info("serving",
Expand Down
76 changes: 76 additions & 0 deletions provider/pkg/provider/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package command

import (
"context"
"fmt"
"strings"

"github.com/pulumi/pulumi-go-provider/infer"
pb "github.com/unmango/pulumi-baremetal/gen/go/unmango/baremetal/v1alpha1"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider/internal/logger"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider/internal/provisioner"
)

type CommandArgs struct {
Args []string `pulumi:"args"`
Triggers []any `pulumi:"triggers,optional"`
}

type Command struct{}

type CommandState struct {
CommandArgs

ExitCode int `pulumi:"exitCode"`
Stdout string `pulumi:"stdout"`
Stderr string `pulumi:"stderr"`
}

// Create implements infer.CustomCreate.
func (Command) Create(ctx context.Context, name string, inputs CommandArgs, preview bool) (string, CommandState, error) {
log := logger.FromContext(ctx)
state := CommandState{}

p, err := provisioner.FromContext(ctx)
if err != nil {
log.Error("Failed creating provisioner")
return name, state, fmt.Errorf("creating provisioner: %w", err)
}

if preview {
if _, err = p.Ping(ctx, &pb.PingRequest{}); err != nil {
log.WarningStatusf("Failed pinging provisioner: %s", err)
}

return name, state, nil
}

display := display(inputs.Args)
log.DebugStatus("Sending exec request to provisioner")
res, err := p.Exec(ctx, &pb.ExecRequest{
Args: inputs.Args,
})
if err != nil {
log.Errorf("command:%s %s", display, err)
return name, state, fmt.Errorf("sending exec request: %w", err)
}

if res.Result.ExitCode > 0 {
log.Error(display)
return name, state, fmt.Errorf("exec failed: %s", res.Result)
}

state.CommandArgs = inputs
state.ExitCode = int(res.Result.ExitCode)
state.Stdout = res.Result.Stdout
state.Stderr = res.Result.Stderr

log.InfoStatus(display)
return name, state, nil
}

var _ = (infer.CustomCreate[CommandArgs, CommandState])((*Command)(nil))

func display(args []string) string {
return strings.Join(args, " ")
}
2 changes: 1 addition & 1 deletion provider/pkg/provisioner/command/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// executing a gRPC endpoint from within another endpoint on the same service.

// I'm not sure how I want to handle the filesystem expectations yet, so this function exists
// but `Update` and `Delete` will continue to delgate to `Create` for now.
// but `Update` and `Delete` will continue to delegate to `Create` for now.

func execute(ctx context.Context, command *pb.Command, log *slog.Logger) (*pb.Result, error) {
if command == nil {
Expand Down
57 changes: 53 additions & 4 deletions provider/pkg/provisioner/command/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

pb "github.com/unmango/pulumi-baremetal/gen/go/unmango/baremetal/v1alpha1"
"github.com/unmango/pulumi-baremetal/provider/pkg/command"
"github.com/unmango/pulumi-baremetal/provider/pkg/internal/opts"
"google.golang.org/grpc"
)
Expand All @@ -35,21 +36,69 @@ func NewServer(options ...opt) *service {
}

func WithLogger(logger *slog.Logger) opt {
return opts.Safe[opt](func(s *service) {
return func(s *service) error {
s.Log = logger
})
return nil
}
}

func WithWhitelist(list []string) opt {
return opts.Safe[opt](func(s *service) {
return func(s *service) error {
s.Whitelist = list
})
return nil
}
}

func (s *service) Register(server *grpc.Server) {
pb.RegisterCommandServiceServer(server, s)
}

func (s *service) Exec(ctx context.Context, req *pb.ExecRequest) (*pb.ExecResponse, error) {
log := s.Log.With("op", "exec")

bin := req.Args[0]
_, err := command.ParseBin(bin)
if err != nil && !slices.Contains(s.Whitelist, bin) {
log.WarnContext(ctx, "refusing to execute command", "bin", bin, "err", err)
return nil, fmt.Errorf("refusing to execute %s %#v", bin, s.Whitelist)
}

args := req.Args[1:]
log = log.With("bin", bin, "args", args)
log.DebugContext(ctx, "building command")
cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stdin = stdinReader(req.Stdin)

if cmd.Err != nil {
log.ErrorContext(ctx, "failed building command", "err", cmd.Err)
return nil, fmt.Errorf("failed building command: %w", cmd.Err)
}

stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
cmd.Stdout, cmd.Stderr = stdout, stderr

log.DebugContext(ctx, "running command", "cmd", cmd.String())
if err = cmd.Run(); err != nil {
log.WarnContext(ctx, "command failed", "err", err)
}

if cmd.ProcessState == nil {
return nil, errors.New("failed to start command")
}

exitCode := cmd.ProcessState.ExitCode()
log.InfoContext(ctx, "finished running command",
"cmd", cmd.String(),
"exit_code", exitCode,
)

return &pb.ExecResponse{Result: &pb.Result{
ExitCode: int32(exitCode),
Stdout: stdout.String(),
Stderr: stderr.String(),
}}, nil
}

func (s *service) Create(ctx context.Context, req *pb.CreateRequest) (res *pb.CreateResponse, err error) {
log := s.Log.With("op", "create")
createdFiles := make([]string, len(req.ExpectCreated))
Expand Down
2 changes: 2 additions & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/pulumi/pulumi-go-provider/middleware/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider/command"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider/coreutils"
"github.com/unmango/pulumi-baremetal/provider/pkg/provider/kubeadm"
)
Expand Down Expand Up @@ -44,6 +45,7 @@ func Provider() p.Provider {
},
Config: infer.Config[provider.Config](),
Resources: []infer.InferredResource{
infer.Resource[command.Command](),
infer.Resource[coreutils.Mktemp](),
infer.Resource[coreutils.Tar](),
infer.Resource[coreutils.Wget](),
Expand Down
98 changes: 98 additions & 0 deletions sdk/dotnet/Command/Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// *** WARNING: this file was generated by pulumi. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Pulumi.Serialization;
using Pulumi;

namespace UnMango.Baremetal.Command
{
[BaremetalResourceType("baremetal:command:Command")]
public partial class Command : global::Pulumi.CustomResource
{
[Output("args")]
public Output<ImmutableArray<string>> Args { get; private set; } = null!;

[Output("exitCode")]
public Output<int> ExitCode { get; private set; } = null!;

[Output("stderr")]
public Output<string> Stderr { get; private set; } = null!;

[Output("stdout")]
public Output<string> Stdout { get; private set; } = null!;

[Output("triggers")]
public Output<ImmutableArray<object>> Triggers { get; private set; } = null!;


/// <summary>
/// Create a Command resource with the given unique name, arguments, and options.
/// </summary>
///
/// <param name="name">The unique name of the resource</param>
/// <param name="args">The arguments used to populate this resource's properties</param>
/// <param name="options">A bag of options that control this resource's behavior</param>
public Command(string name, CommandArgs args, CustomResourceOptions? options = null)
: base("baremetal:command:Command", name, args ?? new CommandArgs(), MakeResourceOptions(options, ""))
{
}

private Command(string name, Input<string> id, CustomResourceOptions? options = null)
: base("baremetal:command:Command", name, null, MakeResourceOptions(options, id))
{
}

private static CustomResourceOptions MakeResourceOptions(CustomResourceOptions? options, Input<string>? id)
{
var defaultOptions = new CustomResourceOptions
{
Version = Utilities.Version,
PluginDownloadURL = "github://api.github.com/unmango",
};
var merged = CustomResourceOptions.Merge(defaultOptions, options);
// Override the ID if one was specified for consistency with other language SDKs.
merged.Id = id ?? merged.Id;
return merged;
}
/// <summary>
/// Get an existing Command resource's state with the given name, ID, and optional extra
/// properties used to qualify the lookup.
/// </summary>
///
/// <param name="name">The unique name of the resulting resource.</param>
/// <param name="id">The unique provider ID of the resource to lookup.</param>
/// <param name="options">A bag of options that control this resource's behavior</param>
public static Command Get(string name, Input<string> id, CustomResourceOptions? options = null)
{
return new Command(name, id, options);
}
}

public sealed class CommandArgs : global::Pulumi.ResourceArgs
{
[Input("args", required: true)]
private InputList<string>? _args;
public InputList<string> Args
{
get => _args ?? (_args = new InputList<string>());
set => _args = value;
}

[Input("triggers")]
private InputList<object>? _triggers;
public InputList<object> Triggers
{
get => _triggers ?? (_triggers = new InputList<object>());
set => _triggers = value;
}

public CommandArgs()
{
}
public static new CommandArgs Empty => new CommandArgs();
}
}
Empty file added sdk/dotnet/Command/README.md
Empty file.
Loading

0 comments on commit a7835aa

Please sign in to comment.