Skip to content

Commit

Permalink
feat(sdk): Create sdk version v1alpha2 and add the ability to restric…
Browse files Browse the repository at this point in the history
…t changes to certain files and folders
  • Loading branch information
JesseTatasciore committed Jan 6, 2022
1 parent e7b1aeb commit d21f76a
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
with:
path: .cache/bazel-repo
key: bazel-repo
- name: Validate changed files
run: ./.github/workflows/validate_changed_files.sh
- name: bazel test //...
env:
XDG_CACHE_HOME: .cache/bazel-repo
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/validate_changed_files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/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

git fetch # we need to be able to compare our current changes to the main branch

CHANGED_FILES=$(git diff origin/main --name-only)

# loop through changed files
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

# if the give file matches an invalid filepath and loop has not been continued by the list of valid filepaths then fail
done <<< "${CHANGED_FILES}"
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,
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

0 comments on commit d21f76a

Please sign in to comment.