Skip to content

Commit

Permalink
Add provider store and provider manager code to create new providers (#…
Browse files Browse the repository at this point in the history
…3377)

* Extend the ProviderStore interface with Create and CreateWithTx methods

* Extend ProviderClassManager with a new method GetConfig to retrieve the provider class configuration (TODO: app)

* Pass the providerClass directly to getClassManager instead of db.Provider

* Add CreateFromConfig to provider manager
  • Loading branch information
jhrozek authored May 20, 2024
1 parent fe321d3 commit e554e73
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 4 deletions.
19 changes: 19 additions & 0 deletions internal/providers/dockerhub/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package dockerhub
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"slices"
Expand Down Expand Up @@ -110,3 +111,21 @@ func (m *providerClassManager) getProviderCredentials(

return credentials.NewOAuth2TokenCredential(decryptedToken.AccessToken), nil
}

func (m *providerClassManager) GetConfig(
_ context.Context, class db.ProviderClass, userConfig json.RawMessage,
) (json.RawMessage, error) {
if !slices.Contains(m.GetSupportedClasses(), class) {
return nil, fmt.Errorf("provider does not implement %s", string(class))
}

const defaultConfig = `{"dockerhub": {}}`

if len(userConfig) == 0 {
return json.RawMessage(defaultConfig), nil
}

// in the future, we may want to validate the user config and merge it with the default config. Right now
// we just return the user config as is
return userConfig, nil
}
26 changes: 26 additions & 0 deletions internal/providers/github/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package manager
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"slices"
Expand Down Expand Up @@ -292,3 +293,28 @@ type credentialDetails struct {
ownerFilter sql.NullString
isOrg bool
}

func (g *githubProviderManager) GetConfig(
_ context.Context, class db.ProviderClass, userConfig json.RawMessage,
) (json.RawMessage, error) {
if !slices.Contains(g.GetSupportedClasses(), class) {
return nil, fmt.Errorf("provider does not implement %s", string(class))
}

var defaultConfig string
// nolint:exhaustive // we really want handle only the two
switch class {
case db.ProviderClassGithub:
defaultConfig = `{"github": {}}`
case db.ProviderClassGithubApp:
defaultConfig = `{"github-app": {}}`
default:
return nil, fmt.Errorf("unsupported provider class %s", class)
}

if len(userConfig) == 0 {
return json.RawMessage(defaultConfig), nil
}

return userConfig, nil
}
29 changes: 25 additions & 4 deletions internal/providers/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package manager

import (
"context"
"encoding/json"
"errors"
"fmt"

Expand All @@ -32,6 +33,10 @@ import (

// ProviderManager encapsulates operations for manipulating Provider instances
type ProviderManager interface {
// CreateFromConfig creates a new Provider instance in the database with a given configuration or the provider default
CreateFromConfig(
ctx context.Context, providerClass db.ProviderClass, projectID uuid.UUID, name string, config json.RawMessage,
) (*db.Provider, error)
// InstantiateFromID creates the provider from the Provider's UUID
InstantiateFromID(ctx context.Context, providerID uuid.UUID) (v1.Provider, error)
// InstantiateFromNameProject creates the provider using the provider's name and
Expand Down Expand Up @@ -63,6 +68,7 @@ type ProviderManager interface {
// specific Provider class. The idea is that ProviderManager determines the
// class of the Provider, and delegates to the appropraite ProviderClassManager
type ProviderClassManager interface {
GetConfig(ctx context.Context, class db.ProviderClass, userConfig json.RawMessage) (json.RawMessage, error)
// Build creates an instance of Provider based on the config in the DB
Build(ctx context.Context, config *db.Provider) (v1.Provider, error)
// Delete deletes an instance of this provider
Expand Down Expand Up @@ -107,6 +113,22 @@ func NewProviderManager(
}, nil
}

func (p *providerManager) CreateFromConfig(
ctx context.Context, providerClass db.ProviderClass, projectID uuid.UUID, name string, config json.RawMessage,
) (*db.Provider, error) {
manager, err := p.getClassManager(providerClass)
if err != nil {
return nil, fmt.Errorf("error getting class manager: %w", err)
}

provConfig, err := manager.GetConfig(ctx, providerClass, config)
if err != nil {
return nil, fmt.Errorf("error getting provider config: %w", err)
}

return p.store.Create(ctx, providerClass, name, projectID, provConfig)
}

func (p *providerManager) InstantiateFromID(ctx context.Context, providerID uuid.UUID) (v1.Provider, error) {
config, err := p.store.GetByID(ctx, providerID)
if err != nil {
Expand Down Expand Up @@ -170,7 +192,7 @@ func (p *providerManager) DeleteByName(ctx context.Context, name string, project
}

func (p *providerManager) deleteByRecord(ctx context.Context, config *db.Provider) error {
manager, err := p.getClassManager(config)
manager, err := p.getClassManager(config.Class)
if err != nil {
return err
}
Expand All @@ -188,15 +210,14 @@ func (p *providerManager) deleteByRecord(ctx context.Context, config *db.Provide
}

func (p *providerManager) buildFromDBRecord(ctx context.Context, config *db.Provider) (v1.Provider, error) {
manager, err := p.getClassManager(config)
manager, err := p.getClassManager(config.Class)
if err != nil {
return nil, err
}
return manager.Build(ctx, config)
}

func (p *providerManager) getClassManager(config *db.Provider) (ProviderClassManager, error) {
class := config.Class
func (p *providerManager) getClassManager(class db.ProviderClass) (ProviderClassManager, error) {
manager, ok := p.classManagers[class]
if !ok {
return nil, fmt.Errorf("unexpected provider class: %s", class)
Expand Down
57 changes: 57 additions & 0 deletions internal/providers/manager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package manager_test

import (
"context"
"encoding/json"
"errors"
"testing"

Expand All @@ -30,6 +31,62 @@ import (
"github.com/stacklok/minder/internal/providers/mock/fixtures"
)

func TestProviderManager_CreateFromConfig(t *testing.T) {
t.Parallel()

scenarios := []struct {
Name string
Provider *db.Provider
Config json.RawMessage
ExpectedError string
}{
{
Name: "CreateFromConfig returns error when provider class has no associated manager",
Provider: githubAppProvider,
ExpectedError: "unexpected provider class",
},
{
Name: "CreateFromConfig creates a github provider with default configuration",
Provider: githubProvider,
Config: json.RawMessage(`{ github: {} }`),
},
{
Name: "CreateFromConfig creates a github provider with custom configuration",
Provider: githubProvider,
Config: json.RawMessage(`{ github: { key: value} }`),
},
}

for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()

store := fixtures.NewProviderStoreMock()(ctrl)

classManager := mockmanager.NewMockProviderClassManager(ctrl)
classManager.EXPECT().GetSupportedClasses().Return([]db.ProviderClass{db.ProviderClassGithub}).MaxTimes(1)
classManager.EXPECT().GetConfig(gomock.Any(), scenario.Provider.Class, gomock.Any()).Return(scenario.Config, nil).MaxTimes(1)

store.EXPECT().Create(gomock.Any(), scenario.Provider.Class, scenario.Provider.Name, scenario.Provider.ProjectID, scenario.Config).Return(scenario.Provider, nil).MaxTimes(1)

provManager, err := manager.NewProviderManager(store, classManager)
require.NoError(t, err)

newProv, err := provManager.CreateFromConfig(ctx, scenario.Provider.Class, scenario.Provider.ProjectID, scenario.Provider.Name, scenario.Config)
if scenario.ExpectedError != "" {
require.ErrorContains(t, err, scenario.ExpectedError)
} else {
require.NoError(t, err)
scenario.Provider.Definition = scenario.Config
require.Equal(t, scenario.Provider, newProv)
}
})
}
}

// Test both create by name/project, and create by ID together.
// This is because the test logic is basically identical.
func TestProviderManager_Instantiate(t *testing.T) {
Expand Down
31 changes: 31 additions & 0 deletions internal/providers/manager/mock/manager.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions internal/providers/mock/store.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions internal/providers/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package providers
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"

Expand All @@ -32,6 +33,10 @@ import (

// ProviderStore provides methods for retrieving Providers from the database
type ProviderStore interface {
// Create creates a new provider in the database
Create(
ctx context.Context, providerClass db.ProviderClass, name string, projectID uuid.UUID, config json.RawMessage,
) (*db.Provider, error)
// GetByID returns the provider identified by its UUID primary key.
// This should only be used in places when it is certain that the requester
// is authorized to access this provider.
Expand Down Expand Up @@ -90,6 +95,38 @@ func NewProviderStore(store db.Store) ProviderStore {
return &providerStore{store: store}
}

func (p *providerStore) Create(
ctx context.Context,
providerClass db.ProviderClass,
name string,
projectID uuid.UUID,
config json.RawMessage,
) (*db.Provider, error) {
if projectID == uuid.Nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid arguments")
}

providerDef, err := GetProviderClassDefinition(string(providerClass))
if err != nil {
return nil, fmt.Errorf("error getting provider definition: %w", err)
}

provParams := db.CreateProviderParams{
Name: name,
ProjectID: projectID,
Class: providerClass,
Implements: providerDef.Traits,
Definition: config,
AuthFlows: providerDef.AuthorizationFlows,
}

prov, err := p.store.CreateProvider(ctx, provParams)
if err != nil {
return nil, fmt.Errorf("error creating provider: %w", err)
}
return &prov, nil
}

func (p *providerStore) GetByID(ctx context.Context, providerID uuid.UUID) (*db.Provider, error) {
provider, err := p.store.GetProviderByID(ctx, providerID)
if err != nil {
Expand Down

0 comments on commit e554e73

Please sign in to comment.