Skip to content

feat: Support caching provisioner assets #574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"time"

Expand Down Expand Up @@ -42,6 +43,7 @@ func start() *cobra.Command {
var (
accessURL string
address string
cacheDir string
dev bool
postgresURL string
provisionerDaemonCount uint8
Expand Down Expand Up @@ -164,7 +166,7 @@ func start() *cobra.Command {

provisionerDaemons := make([]*provisionerd.Server, 0)
for i := uint8(0); i < provisionerDaemonCount; i++ {
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger)
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger, cacheDir)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
}
Expand Down Expand Up @@ -312,6 +314,12 @@ func start() *cobra.Command {
}
root.Flags().StringVarP(&accessURL, "access-url", "", os.Getenv("CODER_ACCESS_URL"), "Specifies the external URL to access Coder (uses $CODER_ACCESS_URL).")
root.Flags().StringVarP(&address, "address", "a", defaultAddress, "The address to serve the API and dashboard (uses $CODER_ADDRESS).")
// systemd uses the CACHE_DIRECTORY environment variable!
defaultCacheDir := os.Getenv("CACHE_DIRECTORY")
if defaultCacheDir == "" {
defaultCacheDir = filepath.Join(os.TempDir(), ".coder-cache")
}
root.Flags().StringVarP(&cacheDir, "cache-dir", "", defaultCacheDir, "Specify a directory to cache binaries for provision operations.")
defaultDev, _ := strconv.ParseBool(os.Getenv("CODER_DEV_MODE"))
root.Flags().BoolVarP(&dev, "dev", "", defaultDev, "Serve Coder in dev mode for tinkering (uses $CODER_DEV_MODE).")
root.Flags().StringVarP(&postgresURL, "postgres-url", "", "",
Expand Down Expand Up @@ -381,14 +389,15 @@ func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Roo
return nil
}

func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (*provisionerd.Server, error) {
func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger, cacheDir string) (*provisionerd.Server, error) {
terraformClient, terraformServer := provisionersdk.TransportPipe()
go func() {
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
},
Logger: logger,
CachePath: cacheDir,
Logger: logger,
})
if err != nil {
panic(err)
Expand Down
41 changes: 33 additions & 8 deletions cli/workspaceagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"net/http"
"net/url"
"os"
"time"
Expand Down Expand Up @@ -38,6 +39,11 @@ func workspaceAgent() *cobra.Command {
}
logger := slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug)
client := codersdk.New(coderURL)

// exchangeToken returns a session token.
// This is abstracted to allow for the same looping condition
// regardless of instance identity auth type.
var exchangeToken func(context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error)
switch auth {
case "token":
sessionToken, exists := os.LookupEnv("CODER_TOKEN")
Expand All @@ -53,29 +59,48 @@ func workspaceAgent() *cobra.Command {
if gcpClientRaw != nil {
gcpClient, _ = gcpClientRaw.(*metadata.Client)
}
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
return client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient)
}
case "aws-instance-identity":
// This is *only* done for testing to mock client authentication.
// This will never be set in a production scenario.
var awsClient *http.Client
awsClientRaw := cmd.Context().Value("aws-client")
if awsClientRaw != nil {
awsClient, _ = awsClientRaw.(*http.Client)
}
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
return client.AuthWorkspaceAWSInstanceIdentity(ctx, awsClient)
}
case "azure-instance-identity":
return xerrors.Errorf("not implemented")
}

ctx, cancelFunc := context.WithTimeout(cmd.Context(), 30*time.Second)
if exchangeToken != nil {
// Agent's can start before resources are returned from the provisioner
// daemon. If there are many resources being provisioned, this time
// could be significant. This is arbitrarily set at an hour to prevent
// tons of idle agents from pinging coderd.
ctx, cancelFunc := context.WithTimeout(cmd.Context(), time.Hour)
defer cancelFunc()
for retry.New(100*time.Millisecond, 5*time.Second).Wait(ctx) {
var response codersdk.WorkspaceAgentAuthenticateResponse

response, err = client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient)
response, err = exchangeToken(ctx)
if err != nil {
logger.Warn(ctx, "authenticate workspace with Google Instance Identity", slog.Error(err))
logger.Warn(ctx, "authenticate workspace", slog.F("method", auth), slog.Error(err))
continue
}
client.SessionToken = response.SessionToken
logger.Info(ctx, "authenticated with Google Instance Identity")
logger.Info(ctx, "authenticated", slog.F("method", auth))
break
}
if err != nil {
return xerrors.Errorf("agent failed to authenticate in time: %w", err)
}
case "aws-instance-identity":
return xerrors.Errorf("not implemented")
case "azure-instance-identity":
return xerrors.Errorf("not implemented")
}

closer := agent.New(client.ListenWorkspaceAgent, &peer.ConnOptions{
Logger: logger,
})
Expand Down
56 changes: 55 additions & 1 deletion cli/workspaceagent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,66 @@ import (

func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
t.Run("AWS", func(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
client := coderdtest.New(t, &coderdtest.Options{
AWSInstanceIdentity: certificates,
})
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agent: &proto.Agent{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
},
}},
},
},
}},
})
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)

cmd, _ := clitest.New(t, "workspaces", "agent", "--auth", "aws-instance-identity", "--url", client.URL.String())
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
go func() {
// A linting error occurs for weakly typing the context value here,
// but it seems reasonable for a one-off test.
// nolint
ctx = context.WithValue(ctx, "aws-client", metadataClient)
err := cmd.ExecuteContext(ctx)
require.NoError(t, err)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
require.NoError(t, err)
cancelFunc()
})

t.Run("GoogleCloud", func(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
GoogleInstanceIdentity: validator,
})
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
Expand Down
1 change: 1 addition & 0 deletions coder.service
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
CacheDirectory=coder
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ExecStart=/usr/bin/coder start
Expand Down
Loading