Skip to content

feat: reuse agent tokens when a prebuilt agent reinitializes #374

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/docker/docker v26.1.5+incompatible
github.com/google/uuid v1.6.0
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1
github.com/masterminds/semver v1.5.0
github.com/mitchellh/mapstructure v1.5.0
Expand Down Expand Up @@ -50,7 +51,6 @@ require (
github.com/hashicorp/terraform-exec v0.22.0 // indirect
github.com/hashicorp/terraform-json v0.24.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
Expand Down
3 changes: 2 additions & 1 deletion integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ func TestIntegration(t *testing.T) {
"workspace_owner.ssh_private_key": `(?s)^.+?BEGIN OPENSSH PRIVATE KEY.+?END OPENSSH PRIVATE KEY.+?$`,
"workspace_owner.ssh_public_key": `(?s)^ssh-ed25519.+$`,
"workspace_owner.login_type": `password`,
"workspace_owner.rbac_roles": `(?is)\[(\{"name":"[a-z0-9-:]+","org_id":"[a-f0-9-]+"\},?)+\]`,
// org_id will either be a uuid or an empty string for site wide roles.
"workspace_owner.rbac_roles": `(?is)\[(\{"name":"[a-z0-9-:]+","org_id":"[a-f0-9-]*"\},?)+\]`,
},
},
{
Expand Down
51 changes: 46 additions & 5 deletions provider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package provider

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"path/filepath"
"reflect"
"strings"

"github.com/google/uuid"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -22,10 +25,12 @@ func agentResource() *schema.Resource {
SchemaVersion: 1,

Description: "Use this resource to associate an agent.",
CreateContext: func(_ context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
// This should be a real authentication token!
resourceData.SetId(uuid.NewString())
err := resourceData.Set("token", uuid.NewString())
CreateContext: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
agentID := uuid.NewString()
resourceData.SetId(agentID)

token := agentAuthToken(ctx, "")
err := resourceData.Set("token", token)
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -48,10 +53,12 @@ func agentResource() *schema.Resource {
return updateInitScript(resourceData, i)
},
ReadWithoutTimeout: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
err := resourceData.Set("token", uuid.NewString())
token := agentAuthToken(ctx, "")
err := resourceData.Set("token", token)
if err != nil {
return diag.FromErr(err)
}

if _, ok := resourceData.GetOk("display_apps"); !ok {
err = resourceData.Set("display_apps", []interface{}{
map[string]bool{
Expand Down Expand Up @@ -469,3 +476,37 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia
}
return nil
}

func agentAuthToken(ctx context.Context, agentID string) string {
existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID))
if existingToken == "" {
// Most of the time, we will generate a new token for the agent.
// In the case of a prebuilt workspace being claimed, we will override with
// an existing token provided below.
token := uuid.NewString()
return token
}

// An existing token was provided for this agent. That means that this
// is a prebuilt workspace in the process of being claimed.
// We should reuse the token.
tflog.Info(ctx, "using provided agent token for prebuild", map[string]interface{}{
"agent_id": agentID,
})
return existingToken
}

// RunningAgentTokenEnvironmentVariable returns the name of an environment variable
// that contains the token to use for the running agent. This is used for prebuilds,
// where we want to reuse the same token for the next iteration of a workspace agent
// before and after the workspace was claimed by a user.
//
// By reusing an existing token, we can avoid the need to change a value that may have been
// used immutably. Thus, allowing us to avoid reprovisioning resources that may take a long time
// to replace.
//
// agentID is unused for now, but will be used as soon as we support multiple agents.
func RunningAgentTokenEnvironmentVariable(agentID string) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will the ID be injected? How will we maintain a persistent identity across terraform apply runs?

sum := sha256.Sum256([]byte(agentID))
return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN_" + hex.EncodeToString(sum[:])
}
28 changes: 23 additions & 5 deletions provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func workspaceDataSource() *schema.Resource {
}
_ = rd.Set("start_count", count)

prebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable())
prebuildCount := 0
if prebuild == "true" {
prebuildCount = 1
if isPrebuiltWorkspace() {
_ = rd.Set("prebuild_count", 1)
_ = rd.Set("is_prebuild", true)
} else {
_ = rd.Set("prebuild_count", 0)
_ = rd.Set("is_prebuild", false)
}
_ = rd.Set("prebuild_count", prebuildCount)

name := helpers.OptionalEnvOrDefault("CODER_WORKSPACE_NAME", "default")
rd.Set("name", name)
Expand Down Expand Up @@ -140,6 +140,24 @@ func workspaceDataSource() *schema.Resource {
}
}

// isPrebuiltWorkspace returns true if the workspace is an unclaimed prebuilt workspace.
func isPrebuiltWorkspace() bool {
return helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true"
}

// IsPrebuildEnvironmentVariable returns the name of the environment variable that
// indicates whether the workspace is an unclaimed prebuilt workspace.
//
// Knowing whether the workspace is an unclaimed prebuilt workspace allows template
// authors to conditionally execute code in the template based on whether the workspace
// has been assigned to a user or not. This allows identity specific configuration to
// be applied only after the workspace is claimed, while the rest of the workspace can
// be pre-configured.
//
// The value of this environment variable should be set to "true" if the workspace is prebuilt
// and it has not yet been claimed by a user. Any other values, including "false"
// and "" will be interpreted to mean that the workspace is not prebuilt, or was
// prebuilt but has since been claimed by a user.
func IsPrebuildEnvironmentVariable() string {
return "CODER_WORKSPACE_IS_PREBUILD"
}
Loading