Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

feat: mock git plugin provider #3

Merged
merged 12 commits into from
Sep 6, 2024
4 changes: 2 additions & 2 deletions kontrol-service/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (sv *Server) DeleteTenantUuidFlowFlowId(_ context.Context, request api.Dele

if flowTopology, found := allFlows[request.FlowId]; found {
logrus.Infof("deleting flow %s", request.FlowId)
pluginRunner := plugins.NewPluginRunner(request.Uuid, sv.db)
pluginRunner := plugins.NewPluginRunner(plugins.NewGitPluginProviderImpl(), request.Uuid, sv.db)
err := flow.DeleteFlow(pluginRunner, flowTopology, request.FlowId)
if err != nil {
errMsg := fmt.Sprintf("An error occurred deleting flow '%v'", request.FlowId)
Expand Down Expand Up @@ -453,7 +453,7 @@ func applyProdDevFlow(sv *Server, tenantUuidStr string, patches []flow_spec.Serv
ServicePatches: patches,
}

pluginRunner := plugins.NewPluginRunner(tenantUuidStr, sv.db)
pluginRunner := plugins.NewPluginRunner(plugins.NewGitPluginProviderImpl(), tenantUuidStr, sv.db)
devClusterTopology, err := engine.GenerateProdDevCluster(&baseClusterTopologyMaybeWithTemplateOverrides, baseTopology, pluginRunner, flowSpec)
if err != nil {
return nil, []string{}, err
Expand Down
15 changes: 11 additions & 4 deletions kontrol-service/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"hash/fnv"
"os"
"path/filepath"
"time"

"github.com/DATA-DOG/go-sqlmock"
Expand Down Expand Up @@ -56,14 +58,19 @@ func NewMockDb() (*Db, error) {
}, nil
}

func NewSQLiteDB() (*Db, error) {
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
func NewSQLiteDB() (*Db, func() error, error) {
cwDirPath, err := os.Getwd()
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred opening the connection to the SQLite database")
return nil, nil, stacktrace.Propagate(err, "An error occurred getting current working directory to created sqlite database at.")
}
sqliteDbPath := filepath.Join(cwDirPath, "gorm.db")
db, err := gorm.Open(sqlite.Open(sqliteDbPath), &gorm.Config{})
if err != nil {
return nil, nil, stacktrace.Propagate(err, "An error occurred opening the connection to the SQLite database")
}
return &Db{
db: db,
}, nil
}, func() error { return os.Remove(sqliteDbPath) }, nil
}

func (db *Db) AutoMigrate(dst ...interface{}) error {
Expand Down
23 changes: 14 additions & 9 deletions kontrol-service/engine/flow/dev_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"kardinal.kontrol-service/types/flow_spec"
)

const dummyPluginName = "https://github.com/kurtosis-tech/dummy-plugin"
const dummyPluginName = "https://github.com/h4ck3rk3y/identity-plugin.git"

func clusterTopologyExample() resolved.ClusterTopology {
dummySpec := &appsv1.DeploymentSpec{}
Expand Down Expand Up @@ -409,8 +409,8 @@ func assertStatefulServices(t *testing.T, originalCluster *resolved.ClusterTopol
}
}

func getPluginRunner(t *testing.T) *plugins.PluginRunner {
db, err := database.NewSQLiteDB()
func getPluginRunner(t *testing.T) (*plugins.PluginRunner, func() error) {
db, cleanUpDbFunc, err := database.NewSQLiteDB()
require.NoError(t, err)
err = db.Clear()
require.NoError(t, err)
Expand All @@ -419,10 +419,11 @@ func getPluginRunner(t *testing.T) *plugins.PluginRunner {
_, err = db.GetOrCreateTenant("tenant-test")
require.NoError(t, err)
pluginRunner := plugins.NewPluginRunner(
plugins.NewMockGitPluginProvider(plugins.MockGitHub),
"tenant-test",
db,
)
return pluginRunner
return pluginRunner, cleanUpDbFunc
}

func TestTopologyToGraph(t *testing.T) {
Expand Down Expand Up @@ -481,7 +482,9 @@ func TestDeepCopyService(t *testing.T) {
func TestDevFlowImmutability(t *testing.T) {
cluster := clusterTopologyExample()
checkoutservice := getServiceRef(&cluster, "checkoutservice")
pluginRunner := getPluginRunner(t)
pluginRunner, cleanUpDbFunc := getPluginRunner(t)
defer cleanUpDbFunc()

flowSpec := flow_spec.FlowPatch{
FlowId: "dev-flow-1",
ServicePatches: []flow_spec.ServicePatch{
Expand Down Expand Up @@ -530,8 +533,9 @@ func TestDevFlowImmutability(t *testing.T) {
func TestFlowMerging(t *testing.T) {
cluster := clusterTopologyExample()
checkoutservice := getServiceRef(&cluster, "checkoutservice")
pluginRunner, cleanUpDbFunc := getPluginRunner(t)
defer cleanUpDbFunc()

pluginRunner := getPluginRunner(t)
flowSpec := flow_spec.FlowPatch{
FlowId: "dev-flow-1",
ServicePatches: []flow_spec.ServicePatch{
Expand Down Expand Up @@ -568,9 +572,9 @@ func TestExternalServicesFlowOnDependentService(t *testing.T) {

cartservice, err := cluster.GetService("cartservice")
require.NoError(t, err)
pluginRunner, cleanUpDbFunc := getPluginRunner(t)
defer cleanUpDbFunc()

// TODO: mock the plugin runner so it doesn't pull from github
pluginRunner := getPluginRunner(t)
flowSpec := flow_spec.FlowPatch{
FlowId: "dev-flow-1",
ServicePatches: []flow_spec.ServicePatch{
Expand Down Expand Up @@ -601,8 +605,9 @@ func TestExternalServicesCreateDevFlowOnNotDependentService(t *testing.T) {

frontend, err := cluster.GetService("frontend")
require.NoError(t, err)
pluginRunner, cleanUpDbFunc := getPluginRunner(t)
defer cleanUpDbFunc()

pluginRunner := getPluginRunner(t)
flowSpec := flow_spec.FlowPatch{
FlowId: "dev-flow-1",
ServicePatches: []flow_spec.ServicePatch{
Expand Down
Binary file removed kontrol-service/kardinal.kontrol-service
Binary file not shown.
71 changes: 71 additions & 0 deletions kontrol-service/plugins/git_plugin_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package plugins

import (
"fmt"
"os"
"os/exec"
"path/filepath"
)

const (
mockProviderPerms = 0644
)

type GitPluginProvider interface {
PullGitHubPlugin(repoPath, repoUrl string) error
}

type GitPluginProviderImpl struct{}

func NewGitPluginProviderImpl() *GitPluginProviderImpl {
return &GitPluginProviderImpl{}
}

func (gpp *GitPluginProviderImpl) PullGitHubPlugin(repoPath, repoUrl string) error {
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
cmd := exec.Command("git", "clone", repoUrl, repoPath)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git clone failed: %v\nOutput: %s", err, output)
}
} else {
// If the repository already exists, pull the latest changes
cmd := exec.Command("git", "-C", repoPath, "pull")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git pull failed: %v\nOutput: %s", err, output)
}
}
return nil
}

type MockGitPluginProvider struct {
// repoURL -> [ filename: fileContents ]
github map[string]map[string]string
}

func NewMockGitPluginProvider(github map[string]map[string]string) *MockGitPluginProvider {
return &MockGitPluginProvider{
github: github,
}
}

func (mgpp *MockGitPluginProvider) PullGitHubPlugin(repoPath, repoUrl string) error {
repoContents, found := mgpp.github[repoUrl]
if !found {
return fmt.Errorf("Repo with url '%v' not found in github", repoUrl)
}
// repoPath should already exist but in case, create it
err := os.MkdirAll(repoPath, 0744)
if err != nil {
return fmt.Errorf("An error occurred ensuring directory for '%v' exists:\n%v", repoUrl, err.Error())
}

for filename, contents := range repoContents {
filePath := filepath.Join(repoPath, filename)

err := os.WriteFile(filePath, []byte(contents), mockProviderPerms)
if err != nil {
return fmt.Errorf("An error occurred writing to filepath '%v' with contents:\n%v", repoPath, contents)
}
}
return nil
}
83 changes: 83 additions & 0 deletions kontrol-service/plugins/mock_github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package plugins

var MockGitHub = map[string]map[string]string{
// Simple Plugin
"https://github.com/h4ck3rk3y/a-test-plugin.git": {
"main.py": `REPLACED = "the-text-has-been-replaced"

def create_flow(service_spec, deployment_spec, flow_uuid, text_to_replace):
deployment_spec['template']['metadata']['labels']['app'] = deployment_spec['template']['metadata']['labels']['app'].replace(text_to_replace, REPLACED)
deployment_spec['selector']['matchLabels']['app'] = deployment_spec['selector']['matchLabels']['app'].replace(text_to_replace, REPLACED)
deployment_spec['template']['spec']['containers'][0]['name'] = deployment_spec['template']['spec']['containers'][0]['name'].replace(text_to_replace, REPLACED)

config_map = {
"original_text": text_to_replace
}

return {
"deployment_spec": deployment_spec,
"config_map": config_map
}

def delete_flow(config_map, flow_uuid):
print(config_map["original_text"])
`,
},
// Complex Plugin
"https://github.com/h4ck3rk3y/slightly-more-complex-plugin.git": {
"main.py": `import json
import requests

def create_flow(service_spec, deployment_spec, flow_uuid):
response = requests.get("https://ident.me")
if response.status_code != 200:
raise Exception("An unexpected error occurred")

ip_address = response.text.strip()

# Replace the IP address in the environment variable
for container in deployment_spec['template']['spec']['containers']:
for env in container['env']:
if env['name'] == 'REDIS':
env['value'] = ip_address

config_map = {
"original_value": "ip_addr"
}

return {
"deployment_spec": deployment_spec,
"config_map": config_map
}

def delete_flow(config_map, flow_uuid):
# In this complex plugin, we don't need to do anything for deletion
return None`,
"requirements.txt": "requests",
},
// Identity Plugin
"https://github.com/h4ck3rk3y/identity-plugin.git": {
"main.py": `def create_flow(service_spec, deployment_spec, flow_uuid):
return {
"deployment_spec": deployment_spec,
"config_map": {}
}

def delete_flow(config_map, flow_uuid):
return None
`,
},
// Redis sidecar plugin
"https://github.com/h4ck3rk3y/redis-sidecar-plugin.git": {
"main.py": `def create_flow(service_spec, deployment_spec, flow_uuid):
deployment_spec['template']['spec']['containers'][0]["image"] = "kurtosistech/redis-proxy-overlay:latest"
return {
"deployment_spec": deployment_spec,
"config_map": {}
}

def delete_flow(config_map, flow_uuid):
pass
`,
},
}
33 changes: 14 additions & 19 deletions kontrol-service/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ const (
)

type PluginRunner struct {
gitPluginProvider GitPluginProvider

tenantId string
db *database.Db

db *database.Db
}

func NewPluginRunner(tenantId string, db *database.Db) *PluginRunner {
func NewPluginRunner(gitPluginProvider GitPluginProvider, tenantId string, db *database.Db) *PluginRunner {
return &PluginRunner{
tenantId: tenantId,
db: db,
gitPluginProvider: gitPluginProvider,
tenantId: tenantId,
db: db,
}
}

func (pr *PluginRunner) CreateFlow(pluginUrl string, serviceSpec corev1.ServiceSpec, deploymentSpec appv1.DeploymentSpec, flowUuid string, arguments map[string]string) (appv1.DeploymentSpec, string, error) {
repoPath, err := getOrCloneRepo(pluginUrl)
repoPath, err := pr.getOrCloneRepo(pluginUrl)
if err != nil {
return appv1.DeploymentSpec{}, "", fmt.Errorf("failed to get or clone repository: %v", err)
}
Expand Down Expand Up @@ -88,7 +92,7 @@ func (pr *PluginRunner) CreateFlow(pluginUrl string, serviceSpec corev1.ServiceS
}

func (pr *PluginRunner) DeleteFlow(pluginUrl, flowUuid string, arguments map[string]string) error {
repoPath, err := getOrCloneRepo(pluginUrl)
repoPath, err := pr.getOrCloneRepo(pluginUrl)
if err != nil {
return fmt.Errorf("failed to get or clone repository: %v", err)
}
Expand Down Expand Up @@ -310,7 +314,7 @@ func executePythonScript(venvPath, repoPath, scriptContent string) error {
return nil
}

func getOrCloneRepo(repoURL string) (string, error) {
func (pr *PluginRunner) getOrCloneRepo(repoURL string) (string, error) {
if !strings.HasPrefix(repoURL, "https://") {
repoURL = "https://" + repoURL
}
Expand All @@ -327,18 +331,9 @@ func getOrCloneRepo(repoURL string) (string, error) {
}

repoPath := filepath.Join(tempDir, repoName)

if _, err := os.Stat(repoPath); os.IsNotExist(err) {
cmd := exec.Command("git", "clone", repoURL, repoPath)
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("git clone failed: %v\nOutput: %s", err, output)
}
} else {
// If the repository already exists, pull the latest changes
cmd := exec.Command("git", "-C", repoPath, "pull")
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("git pull failed: %v\nOutput: %s", err, output)
}
err := pr.gitPluginProvider.PullGitHubPlugin(repoPath, repoURL)
if err != nil {
return "", fmt.Errorf("An error occurred pulling plugin from GitHub:\n%v", err.Error())
}

return repoPath, nil
Expand Down
Loading
Loading