Skip to content

Commit

Permalink
feat: Add external provisioner daemons (#4935)
Browse files Browse the repository at this point in the history
* Start to port over provisioner daemons PR

* Move to Enterprise

* Begin adding tests for external registration

* Move provisioner daemons query to enterprise

* Move around provisioner daemons schema

* Add tags to provisioner daemons

* make gen

* Add user local provisioner daemons

* Add provisioner daemons

* Add feature for external daemons

* Add command to start a provisioner daemon

* Add provisioner tags to template push and create

* Rename migration files

* Fix tests

* Fix entitlements test

* PR comments

* Update migration

* Fix FE types
  • Loading branch information
kylecarbs committed Nov 16, 2022
1 parent 66d20ca commit b6703b1
Show file tree
Hide file tree
Showing 51 changed files with 1,094 additions and 371 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"codersdk",
"cronstrue",
"databasefake",
"dbtype",
"DERP",
"derphttp",
"derpmap",
Expand Down
4 changes: 2 additions & 2 deletions cli/deployment/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func newConfig() *codersdk.DeploymentConfig {
Name: "Cache Directory",
Usage: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
Flag: "cache-dir",
Default: defaultCacheDir(),
Default: DefaultCacheDir(),
},
InMemoryDatabase: &codersdk.DeploymentConfigField[bool]{
Name: "In Memory Database",
Expand Down Expand Up @@ -672,7 +672,7 @@ func formatEnv(key string) string {
return "CODER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(key))
}

func defaultCacheDir() string {
func DefaultCacheDir() string {
defaultCacheDir, err := os.UserCacheDir()
if err != nil {
defaultCacheDir = os.TempDir()
Expand Down
2 changes: 1 addition & 1 deletion cli/gitaskpass.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func gitAskpass() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
defer stop()

user, host, err := gitauth.ParseAskpass(args[0])
Expand Down
2 changes: 1 addition & 1 deletion cli/gitssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func gitssh() *cobra.Command {

// Catch interrupt signals to ensure the temporary private
// key file is cleaned up on most cases.
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
defer stop()

// Early check so errors are reported immediately.
Expand Down
4 changes: 2 additions & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
//
// To get out of a graceful shutdown, the user can send
// SIGQUIT with ctrl+\ or SIGKILL with `kill -9`.
notifyCtx, notifyStop := signal.NotifyContext(ctx, interruptSignals...)
notifyCtx, notifyStop := signal.NotifyContext(ctx, InterruptSignals...)
defer notifyStop()

// Clean up idle connections at the end, e.g.
Expand Down Expand Up @@ -946,7 +946,7 @@ func newProvisionerDaemon(
return provisionerd.New(func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
// This debounces calls to listen every second. Read the comment
// in provisionerdserver.go to learn more!
return coderAPI.ListenProvisionerDaemon(ctx, time.Second)
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, time.Second)
}, &provisionerd.Options{
Logger: logger,
PollInterval: 500 * time.Millisecond,
Expand Down
2 changes: 1 addition & 1 deletion cli/signal_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"syscall"
)

var interruptSignals = []os.Signal{
var InterruptSignals = []os.Signal{
os.Interrupt,
syscall.SIGTERM,
syscall.SIGHUP,
Expand Down
2 changes: 1 addition & 1 deletion cli/signal_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import (
"os"
)

var interruptSignals = []os.Signal{os.Interrupt}
var InterruptSignals = []os.Signal{os.Interrupt}
40 changes: 31 additions & 9 deletions cli/templatecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import (

func templateCreate() *cobra.Command {
var (
directory string
provisioner string
parameterFile string
defaultTTL time.Duration
directory string
provisioner string
provisionerTags []string
parameterFile string
defaultTTL time.Duration
)
cmd := &cobra.Command{
Use: "create [name]",
Expand Down Expand Up @@ -87,12 +88,18 @@ func templateCreate() *cobra.Command {
}
spin.Stop()

tags, err := ParseProvisionerTags(provisionerTags)
if err != nil {
return err
}

job, _, err := createValidTemplateVersion(cmd, createValidTemplateVersionArgs{
Client: client,
Organization: organization,
Provisioner: database.ProvisionerType(provisioner),
FileID: resp.ID,
ParameterFile: parameterFile,
Client: client,
Organization: organization,
Provisioner: database.ProvisionerType(provisioner),
FileID: resp.ID,
ParameterFile: parameterFile,
ProvisionerTags: tags,
})
if err != nil {
return err
Expand Down Expand Up @@ -131,6 +138,7 @@ func templateCreate() *cobra.Command {
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.")
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 24*time.Hour, "Specify a default TTL for workspaces created from this template.")
// This is for testing!
err := cmd.Flags().MarkHidden("test.provisioner")
Expand All @@ -154,6 +162,7 @@ type createValidTemplateVersionArgs struct {
// before prompting the user. Set to false to always prompt for param
// values.
ReuseParameters bool
ProvisionerTags map[string]string
}

func createValidTemplateVersion(cmd *cobra.Command, args createValidTemplateVersionArgs, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
Expand All @@ -165,6 +174,7 @@ func createValidTemplateVersion(cmd *cobra.Command, args createValidTemplateVers
FileID: args.FileID,
Provisioner: codersdk.ProvisionerType(args.Provisioner),
ParameterValues: parameters,
ProvisionerTags: args.ProvisionerTags,
}
if args.Template != nil {
req.TemplateID = args.Template.ID
Expand Down Expand Up @@ -334,3 +344,15 @@ func prettyDirectoryPath(dir string) string {
}
return pretty
}

func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
tags := map[string]string{}
for _, rawTag := range rawTags {
parts := strings.SplitN(rawTag, "=", 2)
if len(parts) < 2 {
return nil, xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag)
}
tags[parts[0]] = parts[1]
}
return tags, nil
}
17 changes: 12 additions & 5 deletions cli/templatepush.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import (

func templatePush() *cobra.Command {
var (
directory string
versionName string
provisioner string
parameterFile string
alwaysPrompt bool
directory string
versionName string
provisioner string
parameterFile string
alwaysPrompt bool
provisionerTags []string
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -75,6 +76,11 @@ func templatePush() *cobra.Command {
}
spin.Stop()

tags, err := ParseProvisionerTags(provisionerTags)
if err != nil {
return err
}

job, _, err := createValidTemplateVersion(cmd, createValidTemplateVersionArgs{
Name: versionName,
Client: client,
Expand All @@ -84,6 +90,7 @@ func templatePush() *cobra.Command {
ParameterFile: parameterFile,
Template: &template,
ReuseParameters: !alwaysPrompt,
ProvisionerTags: tags,
})
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions coderd/autobuild/executor/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
Type: database.ProvisionerJobTypeWorkspaceBuild,
StorageMethod: priorJob.StorageMethod,
FileID: priorJob.FileID,
Tags: priorJob.Tags,
Input: input,
})
if err != nil {
Expand Down
98 changes: 84 additions & 14 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package coderd

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
Expand All @@ -18,10 +20,13 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/tailcfg"
Expand All @@ -32,17 +37,20 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/awsidentity"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbtype"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/metricscache"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/wsconncache"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionerd/proto"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/site"
"github.com/coder/coder/tailnet"
)
Expand Down Expand Up @@ -323,13 +331,6 @@ func New(options *Options) *API {
r.Get("/{fileID}", api.fileByID)
r.Post("/", api.postFile)
})

r.Route("/provisionerdaemons", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
)
r.Get("/", api.provisionerDaemons)
})
r.Route("/organizations", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
Expand Down Expand Up @@ -595,18 +596,20 @@ type API struct {
// RootHandler serves "/"
RootHandler chi.Router

metricsCache *metricscache.Cache
siteHandler http.Handler
websocketWaitMutex sync.Mutex
websocketWaitGroup sync.WaitGroup
metricsCache *metricscache.Cache
siteHandler http.Handler

WebsocketWaitMutex sync.Mutex
WebsocketWaitGroup sync.WaitGroup

workspaceAgentCache *wsconncache.Cache
}

// Close waits for all WebSocket connections to drain before returning.
func (api *API) Close() error {
api.websocketWaitMutex.Lock()
api.websocketWaitGroup.Wait()
api.websocketWaitMutex.Unlock()
api.WebsocketWaitMutex.Lock()
api.WebsocketWaitGroup.Wait()
api.WebsocketWaitMutex.Unlock()

api.metricsCache.Close()
coordinator := api.TailnetCoordinator.Load()
Expand Down Expand Up @@ -635,3 +638,70 @@ func compressHandler(h http.Handler) http.Handler {

return cmp.Handler(h)
}

// CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd. Useful when starting coderd and provisionerd
// in the same process.
func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce time.Duration) (client proto.DRPCProvisionerDaemonClient, err error) {
clientSession, serverSession := provisionersdk.TransportPipe()
defer func() {
if err != nil {
_ = clientSession.Close()
_ = serverSession.Close()
}
}()

name := namesgenerator.GetRandomName(1)
daemon, err := api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{
ID: uuid.New(),
CreatedAt: database.Now(),
Name: name,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
Tags: dbtype.StringMap{
provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
},
})
if err != nil {
return nil, xerrors.Errorf("insert provisioner daemon %q: %w", name, err)
}

tags, err := json.Marshal(daemon.Tags)
if err != nil {
return nil, xerrors.Errorf("marshal tags: %w", err)
}

mux := drpcmux.New()
err = proto.DRPCRegisterProvisionerDaemon(mux, &provisionerdserver.Server{
AccessURL: api.AccessURL,
ID: daemon.ID,
Database: api.Database,
Pubsub: api.Pubsub,
Provisioners: daemon.Provisioners,
Telemetry: api.Telemetry,
Tags: tags,
QuotaCommitter: &api.QuotaCommitter,
AcquireJobDebounce: debounce,
Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
})
if err != nil {
return nil, err
}
server := drpcserver.NewWithOptions(mux, drpcserver.Options{
Log: func(err error) {
if xerrors.Is(err, io.EOF) {
return
}
api.Logger.Debug(ctx, "drpc server error", slog.Error(err))
},
})
go func() {
err := server.Serve(ctx, serverSession)
if err != nil && !xerrors.Is(err, io.EOF) {
api.Logger.Debug(ctx, "provisioner daemon disconnected", slog.Error(err))
}
// close the sessions so we don't leak goroutines serving them.
_ = clientSession.Close()
_ = serverSession.Close()
}()

return proto.NewDRPCProvisionerDaemonClient(provisionersdk.Conn(clientSession)), nil
}
16 changes: 0 additions & 16 deletions coderd/coderdtest/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)

func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
Expand Down Expand Up @@ -204,11 +203,6 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
"GET:/api/v2/provisionerdaemons": {
StatusCode: http.StatusOK,
AssertObject: rbac.ResourceProvisionerDaemon,
},

"POST:/api/v2/parameters/{scope}/{id}": {
AssertAction: rbac.ActionUpdate,
AssertObject: rbac.ResourceTemplate,
Expand Down Expand Up @@ -303,16 +297,6 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
if !ok {
t.Fail()
}
// The provisioner will call to coderd and register itself. This is async,
// so we wait for it to occur.
require.Eventually(t, func() bool {
provisionerds, err := client.ProvisionerDaemons(ctx)
return assert.NoError(t, err) && len(provisionerds) > 0
}, testutil.WaitLong, testutil.IntervalSlow)

provisionerds, err := client.ProvisionerDaemons(ctx)
require.NoError(t, err, "fetch provisioners")
require.Len(t, provisionerds, 1)

organization, err := client.Organization(ctx, admin.OrganizationID)
require.NoError(t, err, "fetch org")
Expand Down

0 comments on commit b6703b1

Please sign in to comment.