From d21f76ac11a0d579b6f51a52d17fb68e2f621ddd Mon Sep 17 00:00:00 2001 From: Jesse Tatasciore Date: Thu, 6 Jan 2022 17:58:36 -0500 Subject: [PATCH] feat(sdk): Create sdk version v1alpha2 and add the ability to restrict changes to certain files and folders --- .github/workflows/ci.yml | 2 + .github/workflows/validate_changed_files.sh | 31 ++++ pkg/plugin/sdk/v1alpha2/README.md | 6 + pkg/plugin/sdk/v1alpha2/config/BUILD.bazel | 12 ++ pkg/plugin/sdk/v1alpha2/config/config.go | 41 +++++ pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel | 19 +++ pkg/plugin/sdk/v1alpha2/plugin/grpc.go | 176 ++++++++++++++++++++ pkg/plugin/sdk/v1alpha2/plugin/interface.go | 21 +++ pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel | 26 +++ pkg/plugin/sdk/v1alpha2/proto/dummy.go | 6 + pkg/plugin/sdk/v1alpha2/proto/plugin.proto | 68 ++++++++ 11 files changed, 408 insertions(+) create mode 100755 .github/workflows/validate_changed_files.sh create mode 100644 pkg/plugin/sdk/v1alpha2/README.md create mode 100644 pkg/plugin/sdk/v1alpha2/config/BUILD.bazel create mode 100644 pkg/plugin/sdk/v1alpha2/config/config.go create mode 100644 pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel create mode 100644 pkg/plugin/sdk/v1alpha2/plugin/grpc.go create mode 100644 pkg/plugin/sdk/v1alpha2/plugin/interface.go create mode 100644 pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel create mode 100644 pkg/plugin/sdk/v1alpha2/proto/dummy.go create mode 100644 pkg/plugin/sdk/v1alpha2/proto/plugin.proto diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29f82e37c..3a494af28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/validate_changed_files.sh b/.github/workflows/validate_changed_files.sh new file mode 100755 index 000000000..a9e65bd46 --- /dev/null +++ b/.github/workflows/validate_changed_files.sh @@ -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}" \ No newline at end of file diff --git a/pkg/plugin/sdk/v1alpha2/README.md b/pkg/plugin/sdk/v1alpha2/README.md new file mode 100644 index 000000000..8b9619d63 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/README.md @@ -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. diff --git a/pkg/plugin/sdk/v1alpha2/config/BUILD.bazel b/pkg/plugin/sdk/v1alpha2/config/BUILD.bazel new file mode 100644 index 000000000..16ffc5c21 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/config/BUILD.bazel @@ -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", + ], +) diff --git a/pkg/plugin/sdk/v1alpha2/config/config.go b/pkg/plugin/sdk/v1alpha2/config/config.go new file mode 100644 index 000000000..f7a45c281 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/config/config.go @@ -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, + } +} diff --git a/pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel b/pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel new file mode 100644 index 000000000..0420486d4 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/plugin/BUILD.bazel @@ -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", + ], +) diff --git a/pkg/plugin/sdk/v1alpha2/plugin/grpc.go b/pkg/plugin/sdk/v1alpha2/plugin/grpc.go new file mode 100644 index 000000000..c144a73c1 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/plugin/grpc.go @@ -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 +} diff --git a/pkg/plugin/sdk/v1alpha2/plugin/interface.go b/pkg/plugin/sdk/v1alpha2/plugin/interface.go new file mode 100644 index 000000000..aac80cec2 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/plugin/interface.go @@ -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 +} diff --git a/pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel b/pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel new file mode 100644 index 000000000..f4a33c018 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/proto/BUILD.bazel @@ -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"], +) diff --git a/pkg/plugin/sdk/v1alpha2/proto/dummy.go b/pkg/plugin/sdk/v1alpha2/proto/dummy.go new file mode 100644 index 000000000..7ecf3b15f --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/proto/dummy.go @@ -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 diff --git a/pkg/plugin/sdk/v1alpha2/proto/plugin.proto b/pkg/plugin/sdk/v1alpha2/proto/plugin.proto new file mode 100644 index 000000000..b2a81b053 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha2/proto/plugin.proto @@ -0,0 +1,68 @@ +syntax = "proto3"; + +package proto; + +import "third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto"; + +option go_package = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/proto"; + +// Plugin is the service used by the Core to communicate with a Plugin instance. +service Plugin { + rpc BEPEventCallback(BEPEventCallbackReq) returns (BEPEventCallbackRes); + rpc PostBuildHook(PostBuildHookReq) returns (PostBuildHookRes); +} + +message BEPEventCallbackReq { + build_event_stream.BuildEvent event = 1; +} + +message BEPEventCallbackRes {} + +message PostBuildHookReq { + uint32 broker_id = 1; + bool is_interactive_mode = 2; +} + +message PostBuildHookRes {} + +// Prompter is the service used by the Plugin instances to request prompt +// actions to the Core from the CLI users. +service Prompter { + rpc Run(PromptRunReq) returns (PromptRunRes); +} + +// PromptRunReq maps the relevant values from +// (github.com/manifoldco/promptui).Prompt. +message PromptRunReq { + // Label is the value displayed on the command line prompt. + string label = 1; + // Default is the initial value for the prompt. This value will be displayed + // next to the prompt's label and the user will be able to view or change it + // depending on the options. + string default = 2; + // AllowEdit lets the user edit the default value. If false, any key press + // other than automatically clears the default value. + bool allow_edit = 3; + // Mask is an optional rune that sets which character to display instead of + // the entered characters. This allows hiding private information like + // passwords. + string mask = 5; + // HideEntered sets whether to hide the text after the user has pressed enter. + bool hide_entered = 6; + // IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than + // request an input. When set, most properties related to input will be + // ignored. + bool is_confirm = 8; + // IsVimMode enables vi-like movements (hjkl) and editing. + bool is_vim_mode = 9; +} + +// PromptRunRes maps the returned values from promptui.Run. +message PromptRunRes { + string result = 1; + message Error { + bool happened = 1; + string message = 2; + } + Error error = 2; +}