Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): create sdk version v1alpha2 #99

Merged
merged 3 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
with:
path: .cache/bazel-repo
key: bazel-repo
- name: Validate changed files
if: github.event_name == 'pull_request'
run: ./.github/workflows/validate_changed_files.sh ${{ github.head_ref }} ${{ github.base_ref }}
- name: bazel test //...
env:
XDG_CACHE_HOME: .cache/bazel-repo
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/validate_changed_files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash

set -o errexit -o nounset -o pipefail

INVALID_FILE_PATHS=('pkg/plugin/sdk') # Array of filepaths that are not allowed
VALID_FILE_PATHS=('pkg/plugin/sdk/v1alpha2') # Array of filepaths that are allowed

# by default the only local branch will be pull/PR#/merge
# fetch only the latest commit from the 2 branches in question to avoid fetching the entire repo which could be costly
git fetch --depth 1 origin "$1"
git fetch --depth 1 origin "$2"

git diff "origin/$1..origin/$2" --name-only | while read -r file; do

# check if filepath matches a valid path. If so move to the next change
for valid_path in "${VALID_FILE_PATHS[@]}"; do
if [[ "${file}" == "${valid_path}"* ]]; then
continue 2
fi
done

# check if filepath matches an invalid path
for invalid_path in "${INVALID_FILE_PATHS[@]}"; do
if [[ "${file}" == "${invalid_path}"* ]]; then
echo "Branch contains changes to filepaths that are invalid"
exit 1
fi
done
done
6 changes: 6 additions & 0 deletions pkg/plugin/sdk/v1alpha2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Plugin SDK v1alpha2

This is the SDK for creating plugins for the Aspect CLI using the Go language.

This doc is a **work in progress**. Use the
[fix-visibility plugin](/plugins/fix-visibility) as a reference for now.
12 changes: 12 additions & 0 deletions pkg/plugin/sdk/v1alpha2/config/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "config",
srcs = ["config.go"],
importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/config",
visibility = ["//visibility:public"],
deps = [
"//pkg/plugin/sdk/v1alpha2/plugin",
"@com_github_hashicorp_go_plugin//:go-plugin",
],
)
41 changes: 41 additions & 0 deletions pkg/plugin/sdk/v1alpha2/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright © 2021 Aspect Build Systems Inc

Not licensed for re-use.
*/

package config

import (
goplugin "github.com/hashicorp/go-plugin"

"aspect.build/cli/pkg/plugin/sdk/v1alpha2/plugin"
)

// DefaultPluginName is the name each aspect plugin must provide.
const DefaultPluginName = "aspectplugin"

// Handshake is the shared handshake config for the v1alpha2 protocol.
var Handshake = goplugin.HandshakeConfig{
ProtocolVersion: 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the number of changes we will make to the SDK, this will have to bump. Not in this PR, though.

MagicCookieKey: "PLUGIN",
MagicCookieValue: "ASPECT",
}

// PluginMap represents the plugin interfaces allowed to be implemented by a
// plugin executable.
var PluginMap = map[string]goplugin.Plugin{
DefaultPluginName: &plugin.GRPCPlugin{},
}

// NewConfigFor returns the default configuration for the passed Plugin
// implementation.
func NewConfigFor(p plugin.Plugin) *goplugin.ServeConfig {
return &goplugin.ServeConfig{
HandshakeConfig: Handshake,
Plugins: map[string]goplugin.Plugin{
DefaultPluginName: &plugin.GRPCPlugin{Impl: p},
},
GRPCServer: goplugin.DefaultGRPCServer,
}
}
19 changes: 19 additions & 0 deletions pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "plugin",
srcs = [
"grpc.go",
"interface.go",
],
importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/plugin",
visibility = ["//visibility:public"],
deps = [
"//bazel/buildeventstream/proto",
"//pkg/ioutils",
"//pkg/plugin/sdk/v1alpha2/proto",
"@com_github_hashicorp_go_plugin//:go-plugin",
"@com_github_manifoldco_promptui//:promptui",
"@org_golang_google_grpc//:go_default_library",
],
)
176 changes: 176 additions & 0 deletions pkg/plugin/sdk/v1alpha2/plugin/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
Copyright © 2021 Aspect Build Systems Inc

Not licensed for re-use.
*/

// grpc.go hides all the complexity of doing the gRPC calls between the aspect
// Core and a Plugin implementation by providing simple abstractions from the
// point of view of Plugin maintainers.
package plugin

import (
"context"
"fmt"

goplugin "github.com/hashicorp/go-plugin"
"github.com/manifoldco/promptui"
"google.golang.org/grpc"

buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/sdk/v1alpha2/proto"
)

// GRPCPlugin represents a Plugin that communicates over gRPC.
type GRPCPlugin struct {
goplugin.Plugin
Impl Plugin
}

// GRPCServer registers an instance of the GRPCServer in the Plugin binary.
func (p *GRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterPluginServer(s, &GRPCServer{Impl: p.Impl, broker: broker})
return nil
}

// GRPCClient returns a client to perform the RPC calls to the Plugin
// instance from the Core.
func (p *GRPCPlugin) GRPCClient(ctx context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCClient{client: proto.NewPluginClient(c), broker: broker}, nil
}

// GRPCServer implements the gRPC server that runs on the Plugin instances.
type GRPCServer struct {
Impl Plugin
broker *goplugin.GRPCBroker
}

// BEPEventCallback translates the gRPC call to the Plugin BEPEventCallback
// implementation.
func (m *GRPCServer) BEPEventCallback(
ctx context.Context,
req *proto.BEPEventCallbackReq,
) (*proto.BEPEventCallbackRes, error) {
return &proto.BEPEventCallbackRes{}, m.Impl.BEPEventCallback(req.Event)
}

// PostBuildHook translates the gRPC call to the Plugin PostBuildHook
// implementation. It starts a prompt runner that is passed to the Plugin
// instance to be able to perform prompt actions to the CLI user.
func (m *GRPCServer) PostBuildHook(
ctx context.Context,
req *proto.PostBuildHookReq,
) (*proto.PostBuildHookRes, error) {
conn, err := m.broker.Dial(req.BrokerId)
if err != nil {
return nil, err
}
defer conn.Close()

client := proto.NewPrompterClient(conn)
prompter := &PrompterGRPCClient{client: client}
return &proto.PostBuildHookRes{},
m.Impl.PostBuildHook(req.IsInteractiveMode, prompter)
}

// GRPCClient implements the gRPC client that is used by the Core to communicate
// with the Plugin instances.
type GRPCClient struct {
client proto.PluginClient
broker *goplugin.GRPCBroker
}

// BEPEventCallback is called from the Core to execute the Plugin
// BEPEventCallback.
func (m *GRPCClient) BEPEventCallback(event *buildeventstream.BuildEvent) error {
_, err := m.client.BEPEventCallback(context.Background(), &proto.BEPEventCallbackReq{Event: event})
return err
}

// PostBuildHook is called from the Core to execute the Plugin PostBuildHook. It
// starts the prompt runner server with the provided PromptRunner.
func (m *GRPCClient) PostBuildHook(isInteractiveMode bool, promptRunner ioutils.PromptRunner) error {
prompterServer := &PrompterGRPCServer{promptRunner: promptRunner}
var s *grpc.Server
serverFunc := func(opts []grpc.ServerOption) *grpc.Server {
s = grpc.NewServer(opts...)
proto.RegisterPrompterServer(s, prompterServer)
return s
}
brokerID := m.broker.NextId()
go m.broker.AcceptAndServe(brokerID, serverFunc)
req := &proto.PostBuildHookReq{
BrokerId: brokerID,
IsInteractiveMode: isInteractiveMode,
}
_, err := m.client.PostBuildHook(context.Background(), req)
s.Stop()
return err
}

// PrompterGRPCServer implements the gRPC server that runs on the Core and is
// passed to the Plugin to allow prompt actions to the CLI user.
type PrompterGRPCServer struct {
promptRunner ioutils.PromptRunner
}

// Run translates the gRPC call to perform a prompt Run on the Core.
func (p *PrompterGRPCServer) Run(
ctx context.Context,
req *proto.PromptRunReq,
) (*proto.PromptRunRes, error) {
prompt := promptui.Prompt{
Label: req.GetLabel(),
Default: req.GetDefault(),
AllowEdit: req.GetAllowEdit(),
Mask: []rune(req.GetMask())[0],
HideEntered: req.GetHideEntered(),
IsConfirm: req.GetIsConfirm(),
IsVimMode: req.GetIsVimMode(),
}

result, err := p.promptRunner.Run(prompt)
res := &proto.PromptRunRes{Result: result}
if err != nil {
res.Error = &proto.PromptRunRes_Error{
Happened: true,
Message: err.Error(),
}
}

return res, nil
}

// PrompterGRPCClient implements the gRPC client that is used by the Plugin
// instance to communicate with the Core to request prompt actions from the
// user.
type PrompterGRPCClient struct {
client proto.PrompterClient
}

// Run is called from the Plugin to request the Core to run the given
// promptui.Prompt.
func (p *PrompterGRPCClient) Run(prompt promptui.Prompt) (string, error) {
label, isString := prompt.Label.(string)
if !isString {
return "", fmt.Errorf("label '%+v' must be a string", prompt.Label)
}
req := &proto.PromptRunReq{
Label: label,
Default: prompt.Default,
AllowEdit: prompt.AllowEdit,
Mask: string(prompt.Mask),
HideEntered: prompt.HideEntered,
IsConfirm: prompt.IsConfirm,
IsVimMode: prompt.IsVimMode,
}
res, err := p.client.Run(context.Background(), req)
if err != nil {
return "", err
}
if res.Error != nil && res.Error.Happened {
return "", fmt.Errorf(res.Error.Message)
}
return res.Result, nil
}
21 changes: 21 additions & 0 deletions pkg/plugin/sdk/v1alpha2/plugin/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright © 2021 Aspect Build Systems Inc

Not licensed for re-use.
*/

package plugin

import (
buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
"aspect.build/cli/pkg/ioutils"
)

// Plugin determines how an aspect Plugin should be implemented.
type Plugin interface {
BEPEventCallback(event *buildeventstream.BuildEvent) error
PostBuildHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error
}
26 changes: 26 additions & 0 deletions pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")

proto_library(
name = "proto_proto",
srcs = ["plugin.proto"],
visibility = ["//visibility:public"],
deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto:proto_proto"],
)

go_proto_library(
name = "proto_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/proto",
proto = ":proto_proto",
visibility = ["//visibility:public"],
deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto"],
)

go_library(
name = "proto",
embed = [":proto_go_proto"],
importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/proto",
visibility = ["//visibility:public"],
)
6 changes: 6 additions & 0 deletions pkg/plugin/sdk/v1alpha2/proto/dummy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build dummy
// +build dummy

// This file exists to make the go tooling happy. This package is generated by
// bazel.
package proto
Loading