-
Notifications
You must be signed in to change notification settings - Fork 25
/
plugins.go
215 lines (177 loc) · 6.29 KB
/
plugins.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//
// Copyright 2023 The Chainloop Authors.
//
// 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 plugins
import (
"fmt"
"os/exec"
"sort"
dependencytrack "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/dependency-track/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/discord-webhook/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/guac/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/slack-webhook/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/smtp/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
plugin_sdk "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1/plugin"
"github.com/chainloop-dev/chainloop/internal/servicelogger"
"github.com/go-kratos/kratos/v2/log"
"github.com/hashicorp/go-plugin"
)
type pluginsLoader interface {
load() (sdk.AvailablePlugins, error)
}
// directoryLoader loads plugins from a directory
type directoryLoader struct {
pluginsDir string
logger *log.Helper
initializer PluginInitializer
}
// memoryLoader initializes plugins in memory from an array of factories
type memoryLoader struct {
plugins []sdk.FanOutFactory
logger log.Logger
}
type PluginInitializer interface {
Init(path string) (*sdk.FanOutP, error)
}
// Init a go-plugin plugin
type goPluginInitializer struct{}
// Load the available third party integrations, these integrations can come in the form of
// a) Plugins implemented with go-plugin, compiled as a separate binary and placed in pluginsDir
// b) Built-in plugins implemented as a go modules and loaded in memory
// Important: Plugins have precedence over built-in plugins
func Load(pluginsDir string, l log.Logger) (plugins sdk.AvailablePlugins, err error) {
// Array of built-in plugins to enable which are loaded in host memory dynamically
toEnableBuiltIn := []sdk.FanOutFactory{
dependencytrack.New,
smtp.New,
discord.New,
guac.New,
slack.New,
}
// Load plugins in memory from the array above
memLoader := &memoryLoader{plugins: toEnableBuiltIn, logger: l}
logger := servicelogger.ScopedHelper(l, "plugins")
// Load plugins from a directory
dirLoader := &directoryLoader{
pluginsDir: pluginsDir,
initializer: &goPluginInitializer{},
logger: logger,
}
return doLoad(memLoader, dirLoader, logger)
}
func doLoad(memoryLoader pluginsLoader, dirLoader pluginsLoader, logger *log.Helper) (plugins sdk.AvailablePlugins, err error) {
defer func() {
if err != nil {
// If there is an error, we need to clean up the plugins that were loaded
plugins.Cleanup()
}
}()
// Get actual plugins running processes
plugins, err = dirLoader.load()
if err != nil {
return plugins, fmt.Errorf("failed to load plugins: %w", err)
}
fromMemoryPlugins, err := memoryLoader.load()
if err != nil {
return plugins, fmt.Errorf("failed to load plugins: %w", err)
}
// load built-in plugins but skip them if they are loaded already as actual plugins
// It will enable us to rollout plugins iteratively
BUILT_IN_LOOP:
for _, builtInPlugin := range fromMemoryPlugins {
for _, p := range plugins {
if p.Describe().ID == builtInPlugin.Describe().ID {
logger.Infow("msg", "plugin already loaded", "type", "built-in", "plugin", p.String())
continue BUILT_IN_LOOP
}
}
logger.Infow("msg", "loaded", "type", "built-in", "plugin", builtInPlugin.String())
plugins = append(plugins, &sdk.FanOutP{FanOut: builtInPlugin, DisposeFunc: func() {}})
}
sort.Slice(plugins, func(i, j int) bool {
return plugins[i].Describe().ID < plugins[j].Describe().ID
})
return plugins, nil
}
func (l *memoryLoader) load() (sdk.AvailablePlugins, error) {
res := make(sdk.AvailablePlugins, 0, len(l.plugins))
for _, f := range l.plugins {
p, err := f(l.logger)
if err != nil {
return nil, fmt.Errorf("failed to load built-in plugin: %w", err)
}
res = append(res, &sdk.FanOutP{FanOut: p, DisposeFunc: func() {}})
}
return res, nil
}
func (l *directoryLoader) load() (sdk.AvailablePlugins, error) {
var plugins = make(sdk.AvailablePlugins, 0)
if l.pluginsDir == "" {
return plugins, nil
}
const pluginPrefix = "chainloop-plugin-"
var pluginBlob = fmt.Sprintf("%s*", pluginPrefix)
l.logger.Infow("msg", "loading plugins", "dir", l.pluginsDir, "pattern", pluginBlob)
files, err := plugin.Discover(pluginBlob, l.pluginsDir)
if err != nil {
return nil, fmt.Errorf("failed to discover plugins: %w", err)
}
var pluginsMap = make(map[string]*sdk.FanOutP)
for _, f := range files {
d, err := l.initializer.Init(f)
if err != nil {
return nil, fmt.Errorf("failed to load plugin: %w", err)
}
if _, ok := pluginsMap[d.Describe().ID]; ok {
l.logger.Infow("msg", "duplicate plugin, skipping", "plugin", d.Describe().ID)
d.DisposeFunc()
continue
}
pluginsMap[d.Describe().ID] = d
}
for _, p := range pluginsMap {
l.logger.Infow("msg", "loaded", "type", "plugin", "plugin", p.String())
plugins = append(plugins, p)
}
return plugins, nil
}
func (i *goPluginInitializer) Init(path string) (*sdk.FanOutP, error) {
pluginSet := plugin.PluginSet{
plugin_sdk.PluginName: &plugin_sdk.GRPCFanOutPlugin{},
}
// Plugin load test
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: plugin_sdk.HandshakeConfig,
Plugins: pluginSet,
Cmd: exec.Command(path),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
AutoMTLS: true,
})
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
// Request the plugin
raw, err := rpcClient.Dispense(plugin_sdk.PluginName)
if err != nil {
return nil, err
}
fanOut, ok := raw.(sdk.FanOut)
if !ok {
return nil, fmt.Errorf("plugin %q does not implement the FanOut interface", path)
}
return &sdk.FanOutP{FanOut: fanOut, DisposeFunc: client.Kill}, nil
}