feat: add support for `coder_git_auth` data source (#6334)

* Add git auth providers schema

* Pipe git auth providers to the schema

* Add git auth providers to the API

* Add gitauth endpoint to query authenticated state

* Add endpoint to query git state

* Use BroadcastChannel to automatically authenticate with Git

* Add error validation for submitting the create workspace form

* Fix panic on template dry-run

* Add tests for the template version Git auth endpoint

* Show error if no gitauth is configured

* Add gitauth to cliui

* Fix unused method receiver

* Fix linting errors

* Fix dbauthz querier test

* Fix make gen

* Add JavaScript test for git auth

* Fix bad error message

* Fix provisionerd test race

See https://github.com/coder/coder/actions/runs/4277960646/jobs/7447232814

* Fix requested changes

* Add comment to CreateWorkspacePageView
This commit is contained in:
Kyle Carberry 2023-02-27 10:18:19 -06:00 committed by GitHub
parent 3d8b77d6f1
commit 7f226d4f90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2788 additions and 861 deletions

72
cli/cliui/gitauth.go Normal file
View File

@ -0,0 +1,72 @@
package cliui
import (
"context"
"fmt"
"io"
"time"
"github.com/briandowns/spinner"
"github.com/coder/coder/codersdk"
)
type GitAuthOptions struct {
Fetch func(context.Context) ([]codersdk.TemplateVersionGitAuth, error)
FetchInterval time.Duration
}
func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
if opts.FetchInterval == 0 {
opts.FetchInterval = 500 * time.Millisecond
}
gitAuth, err := opts.Fetch(ctx)
if err != nil {
return err
}
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
spin.Writer = writer
spin.ForceOutput = true
spin.Suffix = " Waiting for Git authentication..."
defer spin.Stop()
ticker := time.NewTicker(opts.FetchInterval)
defer ticker.Stop()
for _, auth := range gitAuth {
if auth.Authenticated {
return nil
}
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.Type.Pretty(), auth.AuthenticateURL)
ticker.Reset(opts.FetchInterval)
spin.Start()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
gitAuth, err := opts.Fetch(ctx)
if err != nil {
return err
}
var authed bool
for _, a := range gitAuth {
if !a.Authenticated || a.ID != auth.ID {
continue
}
authed = true
break
}
// The user authenticated with the provider!
if authed {
break
}
}
spin.Stop()
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.Type.Pretty())
}
return nil
}

55
cli/cliui/gitauth_test.go Normal file
View File

@ -0,0 +1,55 @@
package cliui_test
import (
"context"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
func TestGitAuth(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
ptty := ptytest.New(t)
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
var fetched atomic.Bool
return cliui.GitAuth(cmd.Context(), cmd.OutOrStdout(), cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
defer fetched.Store(true)
return []codersdk.TemplateVersionGitAuth{{
ID: "github",
Type: codersdk.GitProviderGitHub,
Authenticated: fetched.Load(),
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
}}, nil
},
FetchInterval: time.Millisecond,
})
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
done := make(chan struct{})
go func() {
defer close(done)
err := cmd.Execute()
assert.NoError(t, err)
}()
ptty.ExpectMatchContext(ctx, "You must authenticate with")
ptty.ExpectMatchContext(ctx, "https://example.com/gitauth/github")
ptty.ExpectMatchContext(ctx, "Successfully authenticated with GitHub")
<-done
}

View File

@ -1,6 +1,7 @@
package cli
import (
"context"
"fmt"
"io"
"time"
@ -324,6 +325,15 @@ PromptRichParamLoop:
_, _ = fmt.Fprintln(cmd.OutOrStdout())
}
err = cliui.GitAuth(ctx, cmd.OutOrStdout(), cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
return client.TemplateVersionGitAuth(ctx, templateVersion.ID)
},
})
if err != nil {
return nil, xerrors.Errorf("template version git auth: %w", err)
}
// Run a dry-run with the given parameters to check correctness
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: args.NewWorkspaceName,

View File

@ -3,15 +3,21 @@ package cli_test
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@ -603,6 +609,61 @@ func TestCreateValidateRichParameters(t *testing.T) {
})
}
func TestCreateWithGitAuth(t *testing.T) {
t.Parallel()
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
GitAuthProviders: []string{"github"},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}
client := coderdtest.New(t, &coderdtest.Options{
GitAuthConfigs: []*gitauth.Config{{
OAuth2Config: &oauth2Config{},
ID: "github",
Regex: regexp.MustCompile(`github\.com`),
Type: codersdk.GitProviderGitHub,
}},
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()
pty.ExpectMatch("You must authenticate with GitHub to create a workspace")
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
_ = resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
<-doneChan
}
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
return []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
@ -638,3 +699,31 @@ func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Resp
},
}}
}
type oauth2Config struct{}
func (*oauth2Config) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string {
return "/?state=" + url.QueryEscape(state)
}
func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "token",
RefreshToken: "refresh",
Expiry: database.Now().Add(time.Hour),
}, nil
}
func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
return &oauth2TokenSource{}
}
type oauth2TokenSource struct{}
func (*oauth2TokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "token",
RefreshToken: "refresh",
Expiry: database.Now().Add(time.Hour),
}, nil
}

View File

@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"io"
"net/url"
"os"
"strings"
"sync/atomic"
"time"
"github.com/spf13/cobra"
@ -235,6 +237,41 @@ func main() {
},
})
root.AddCommand(&cobra.Command{
Use: "git-auth",
RunE: func(cmd *cobra.Command, args []string) error {
var count atomic.Int32
var githubAuthed atomic.Bool
var gitlabAuthed atomic.Bool
go func() {
// Sleep to display the loading indicator.
time.Sleep(time.Second)
// Swap to true to display success and move onto GitLab.
githubAuthed.Store(true)
// Show the loading indicator again...
time.Sleep(time.Second * 2)
// Complete the auth!
gitlabAuthed.Store(true)
}()
return cliui.GitAuth(cmd.Context(), cmd.OutOrStdout(), cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
count.Add(1)
return []codersdk.TemplateVersionGitAuth{{
ID: "github",
Type: codersdk.GitProviderGitHub,
Authenticated: githubAuthed.Load(),
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
}, {
ID: "gitlab",
Type: codersdk.GitProviderGitLab,
Authenticated: gitlabAuthed.Load(),
AuthenticateURL: "https://example.com/gitauth/gitlab?redirect=" + url.QueryEscape("/gitauth?notify"),
}}, nil
},
})
},
})
err := root.Execute()
if err != nil {
_, _ = fmt.Println(err.Error())

70
coderd/apidoc/docs.go generated
View File

@ -2485,6 +2485,44 @@ const docTemplate = `{
}
}
},
"/templateversions/{templateversion}/gitauth": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Templates"
],
"summary": "Get git auth by template version",
"operationId": "get-git-auth-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionGitAuth"
}
}
}
}
}
},
"/templateversions/{templateversion}/logs": {
"get": {
"security": [
@ -6612,6 +6650,21 @@ const docTemplate = `{
}
}
},
"codersdk.GitProvider": {
"type": "string",
"enum": [
"azure-devops",
"github",
"gitlab",
"bitbucket"
],
"x-enum-varnames": [
"GitProviderAzureDevops",
"GitProviderGitHub",
"GitProviderGitLab",
"GitProviderBitBucket"
]
},
"codersdk.GitSSHKey": {
"type": "object",
"properties": {
@ -7717,6 +7770,23 @@ const docTemplate = `{
}
}
},
"codersdk.TemplateVersionGitAuth": {
"type": "object",
"properties": {
"authenticate_url": {
"type": "string"
},
"authenticated": {
"type": "boolean"
},
"id": {
"type": "string"
},
"type": {
"$ref": "#/definitions/codersdk.GitProvider"
}
}
},
"codersdk.TemplateVersionParameter": {
"type": "object",
"properties": {

View File

@ -2183,6 +2183,40 @@
}
}
},
"/templateversions/{templateversion}/gitauth": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get git auth by template version",
"operationId": "get-git-auth-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionGitAuth"
}
}
}
}
}
},
"/templateversions/{templateversion}/logs": {
"get": {
"security": [
@ -5922,6 +5956,16 @@
}
}
},
"codersdk.GitProvider": {
"type": "string",
"enum": ["azure-devops", "github", "gitlab", "bitbucket"],
"x-enum-varnames": [
"GitProviderAzureDevops",
"GitProviderGitHub",
"GitProviderGitLab",
"GitProviderBitBucket"
]
},
"codersdk.GitSSHKey": {
"type": "object",
"properties": {
@ -6946,6 +6990,23 @@
}
}
},
"codersdk.TemplateVersionGitAuth": {
"type": "object",
"properties": {
"authenticate_url": {
"type": "string"
},
"authenticated": {
"type": "boolean"
},
"id": {
"type": "string"
},
"type": {
"$ref": "#/definitions/codersdk.GitProvider"
}
}
},
"codersdk.TemplateVersionParameter": {
"type": "object",
"properties": {

View File

@ -489,6 +489,7 @@ func New(options *Options) *API {
r.Get("/schema", api.templateVersionSchema)
r.Get("/parameters", api.templateVersionParameters)
r.Get("/rich-parameters", api.templateVersionRichParameters)
r.Get("/gitauth", api.templateVersionGitAuth)
r.Get("/variables", api.templateVersionVariables)
r.Get("/resources", api.templateVersionResources)
r.Get("/logs", api.templateVersionLogs)
@ -805,12 +806,18 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti
}
mux := drpcmux.New()
gitAuthProviders := make([]string, 0, len(api.GitAuthConfigs))
for _, cfg := range api.GitAuthConfigs {
gitAuthProviders = append(gitAuthProviders, cfg.ID)
}
err = proto.DRPCRegisterProvisionerDaemon(mux, &provisionerdserver.Server{
AccessURL: api.AccessURL,
ID: daemon.ID,
Database: api.Database,
Pubsub: api.Pubsub,
Provisioners: daemon.Provisioners,
GitAuthProviders: gitAuthProviders,
Telemetry: api.Telemetry,
Tags: tags,
QuotaCommitter: &api.QuotaCommitter,

View File

@ -720,6 +720,33 @@ func MustWorkspace(t *testing.T, client *codersdk.Client, workspaceID uuid.UUID)
return ws
}
// RequestGitAuthCallback makes a request with the proper OAuth2 state cookie
// to the git auth callback endpoint.
func RequestGitAuthCallback(t *testing.T, providerID string, client *codersdk.Client) *http.Response {
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
state := "somestate"
oauthURL, err := client.URL.Parse(fmt.Sprintf("/gitauth/%s/callback?code=asd&state=%s", providerID, state))
require.NoError(t, err)
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: codersdk.OAuth2StateCookie,
Value: state,
})
req.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenCookie,
Value: client.SessionToken(),
})
res, err := client.HTTPClient.Do(req)
require.NoError(t, err)
t.Cleanup(func() {
_ = res.Body.Close()
})
return res
}
// NewGoogleInstanceIdentity returns a metadata client and ID token validator for faking
// instance authentication for Google Cloud.
// nolint:revive

View File

@ -889,6 +889,28 @@ func (q *querier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, a
return q.db.UpdateTemplateVersionDescriptionByJobID(ctx, arg)
}
func (q *querier) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg database.UpdateTemplateVersionGitAuthProvidersByJobIDParams) error {
// An actor is allowed to update the template version git auth providers if they are authorized to update the template.
tv, err := q.db.GetTemplateVersionByJobID(ctx, arg.JobID)
if err != nil {
return err
}
var obj rbac.Objecter
if !tv.TemplateID.Valid {
obj = rbac.ResourceTemplate.InOrg(tv.OrganizationID)
} else {
tpl, err := q.db.GetTemplateByID(ctx, tv.TemplateID.UUID)
if err != nil {
return err
}
obj = tpl
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, obj); err != nil {
return err
}
return q.db.UpdateTemplateVersionGitAuthProvidersByJobID(ctx, arg)
}
func (q *querier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]database.TemplateGroup, error) {
// An actor is authorized to read template group roles if they are authorized to read the template.
template, err := q.db.GetTemplateByID(ctx, id)
@ -1103,11 +1125,11 @@ func (q *querier) InsertGitAuthLink(ctx context.Context, arg database.InsertGitA
return insert(q.log, q.auth, rbac.ResourceUserData.WithOwner(arg.UserID.String()).WithID(arg.UserID), q.db.InsertGitAuthLink)(ctx, arg)
}
func (q *querier) UpdateGitAuthLink(ctx context.Context, arg database.UpdateGitAuthLinkParams) error {
func (q *querier) UpdateGitAuthLink(ctx context.Context, arg database.UpdateGitAuthLinkParams) (database.GitAuthLink, error) {
fetch := func(ctx context.Context, arg database.UpdateGitAuthLinkParams) (database.GitAuthLink, error) {
return q.db.GetGitAuthLink(ctx, database.GetGitAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
}
return update(q.log, q.auth, fetch, q.db.UpdateGitAuthLink)(ctx, arg)
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGitAuthLink)(ctx, arg)
}
func (q *querier) UpdateUserLink(ctx context.Context, arg database.UpdateUserLinkParams) (database.UserLink, error) {

View File

@ -736,6 +736,18 @@ func (s *MethodTestSuite) TestTemplate() {
Readme: "foo",
}).Asserts(t1, rbac.ActionUpdate).Returns()
}))
s.Run("UpdateTemplateVersionGitAuthProvidersByJobID", s.Subtest(func(db database.Store, check *expects) {
jobID := uuid.New()
t1 := dbgen.Template(s.T(), db, database.Template{})
_ = dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true},
JobID: jobID,
})
check.Args(database.UpdateTemplateVersionGitAuthProvidersByJobIDParams{
JobID: jobID,
GitAuthProviders: []string{},
}).Asserts(t1, rbac.ActionUpdate).Returns()
}))
}
func (s *MethodTestSuite) TestUser() {
@ -881,9 +893,13 @@ func (s *MethodTestSuite) TestUser() {
s.Run("UpdateGitAuthLink", s.Subtest(func(db database.Store, check *expects) {
link := dbgen.GitAuthLink(s.T(), db, database.GitAuthLink{})
check.Args(database.UpdateGitAuthLinkParams{
ProviderID: link.ProviderID,
UserID: link.UserID,
}).Asserts(link, rbac.ActionUpdate).Returns()
ProviderID: link.ProviderID,
UserID: link.UserID,
OAuthAccessToken: link.OAuthAccessToken,
OAuthRefreshToken: link.OAuthRefreshToken,
OAuthExpiry: link.OAuthExpiry,
UpdatedAt: link.UpdatedAt,
}).Asserts(link, rbac.ActionUpdate).Returns(link)
}))
s.Run("UpdateUserLink", s.Subtest(func(db database.Store, check *expects) {
link := dbgen.UserLink(s.T(), db, database.UserLink{})

View File

@ -3277,6 +3277,26 @@ func (q *fakeQuerier) UpdateTemplateVersionDescriptionByJobID(_ context.Context,
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Context, arg database.UpdateTemplateVersionGitAuthProvidersByJobIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, templateVersion := range q.templateVersions {
if templateVersion.JobID != arg.JobID {
continue
}
templateVersion.GitAuthProviders = arg.GitAuthProviders
templateVersion.UpdatedAt = arg.UpdatedAt
q.templateVersions[index] = templateVersion
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
@ -4291,9 +4311,9 @@ func (q *fakeQuerier) InsertGitAuthLink(_ context.Context, arg database.InsertGi
return gitAuthLink, nil
}
func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGitAuthLinkParams) error {
func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGitAuthLinkParams) (database.GitAuthLink, error) {
if err := validateDatabaseType(arg); err != nil {
return err
return database.GitAuthLink{}, err
}
q.mutex.Lock()
@ -4310,8 +4330,10 @@ func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGi
gitAuthLink.OAuthRefreshToken = arg.OAuthRefreshToken
gitAuthLink.OAuthExpiry = arg.OAuthExpiry
q.gitAuthLinks[index] = gitAuthLink
return gitAuthLink, nil
}
return nil
return database.GitAuthLink{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UUID) (int64, error) {

View File

@ -417,9 +417,12 @@ CREATE TABLE template_versions (
name character varying(64) NOT NULL,
readme character varying(1048576) NOT NULL,
job_id uuid NOT NULL,
created_by uuid NOT NULL
created_by uuid NOT NULL,
git_auth_providers text[]
);
COMMENT ON COLUMN template_versions.git_auth_providers IS 'IDs of Git auth providers for a specific template version';
CREATE TABLE templates (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,

View File

@ -0,0 +1,2 @@
ALTER TABLE template_versions
DROP COLUMN git_auth_providers;

View File

@ -0,0 +1,4 @@
ALTER TABLE template_versions
ADD COLUMN git_auth_providers text[];
COMMENT ON COLUMN template_versions.git_auth_providers IS 'IDs of Git auth providers for a specific template version';

View File

@ -1433,6 +1433,8 @@ type TemplateVersion struct {
Readme string `db:"readme" json:"readme"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
// IDs of Git auth providers for a specific template version
GitAuthProviders []string `db:"git_auth_providers" json:"git_auth_providers"`
}
type TemplateVersionParameter struct {

View File

@ -176,7 +176,7 @@ type sqlcQuerier interface {
ParameterValue(ctx context.Context, id uuid.UUID) (ParameterValue, error)
ParameterValues(ctx context.Context, arg ParameterValuesParams) ([]ParameterValue, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) error
UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) (GitAuthLink, error)
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
@ -190,6 +190,7 @@ type sqlcQuerier interface {
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error)
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error
UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)

View File

@ -880,13 +880,13 @@ func (q *sqlQuerier) InsertGitAuthLink(ctx context.Context, arg InsertGitAuthLin
return i, err
}
const updateGitAuthLink = `-- name: UpdateGitAuthLink :exec
const updateGitAuthLink = `-- name: UpdateGitAuthLink :one
UPDATE git_auth_links SET
updated_at = $3,
oauth_access_token = $4,
oauth_refresh_token = $5,
oauth_expiry = $6
WHERE provider_id = $1 AND user_id = $2
WHERE provider_id = $1 AND user_id = $2 RETURNING provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry
`
type UpdateGitAuthLinkParams struct {
@ -898,8 +898,8 @@ type UpdateGitAuthLinkParams struct {
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
}
func (q *sqlQuerier) UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) error {
_, err := q.db.ExecContext(ctx, updateGitAuthLink,
func (q *sqlQuerier) UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) (GitAuthLink, error) {
row := q.db.QueryRowContext(ctx, updateGitAuthLink,
arg.ProviderID,
arg.UserID,
arg.UpdatedAt,
@ -907,7 +907,17 @@ func (q *sqlQuerier) UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLin
arg.OAuthRefreshToken,
arg.OAuthExpiry,
)
return err
var i GitAuthLink
err := row.Scan(
&i.ProviderID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
)
return i, err
}
const deleteGitSSHKey = `-- name: DeleteGitSSHKey :exec
@ -3715,7 +3725,7 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3749,13 +3759,14 @@ func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPrev
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
)
return i, err
}
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3775,13 +3786,14 @@ func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
)
return i, err
}
const getTemplateVersionByJobID = `-- name: GetTemplateVersionByJobID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3801,13 +3813,14 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
)
return i, err
}
const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3833,13 +3846,14 @@ func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context,
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
)
return i, err
}
const getTemplateVersionsByIDs = `-- name: GetTemplateVersionsByIDs :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3865,6 +3879,7 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
); err != nil {
return nil, err
}
@ -3881,7 +3896,7 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
const getTemplateVersionsByTemplateID = `-- name: GetTemplateVersionsByTemplateID :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
FROM
template_versions
WHERE
@ -3945,6 +3960,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
); err != nil {
return nil, err
}
@ -3960,7 +3976,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
}
const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by FROM template_versions WHERE created_at > $1
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers FROM template_versions WHERE created_at > $1
`
func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
@ -3982,6 +3998,7 @@ func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, create
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
); err != nil {
return nil, err
}
@ -4010,7 +4027,7 @@ INSERT INTO
created_by
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers
`
type InsertTemplateVersionParams struct {
@ -4048,6 +4065,7 @@ func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTempla
&i.Readme,
&i.JobID,
&i.CreatedBy,
pq.Array(&i.GitAuthProviders),
)
return i, err
}
@ -4094,6 +4112,27 @@ func (q *sqlQuerier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context
return err
}
const updateTemplateVersionGitAuthProvidersByJobID = `-- name: UpdateTemplateVersionGitAuthProvidersByJobID :exec
UPDATE
template_versions
SET
git_auth_providers = $2,
updated_at = $3
WHERE
job_id = $1
`
type UpdateTemplateVersionGitAuthProvidersByJobIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
GitAuthProviders []string `db:"git_auth_providers" json:"git_auth_providers"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateVersionGitAuthProvidersByJobID, arg.JobID, pq.Array(arg.GitAuthProviders), arg.UpdatedAt)
return err
}
const getTemplateVersionVariables = `-- name: GetTemplateVersionVariables :many
SELECT template_version_id, name, description, type, value, default_value, required, sensitive FROM template_version_variables WHERE template_version_id = $1
`

View File

@ -20,10 +20,10 @@ INSERT INTO git_auth_links (
$7
) RETURNING *;
-- name: UpdateGitAuthLink :exec
-- name: UpdateGitAuthLink :one
UPDATE git_auth_links SET
updated_at = $3,
oauth_access_token = $4,
oauth_refresh_token = $5,
oauth_expiry = $6
WHERE provider_id = $1 AND user_id = $2;
WHERE provider_id = $1 AND user_id = $2 RETURNING *;

View File

@ -102,6 +102,15 @@ SET
WHERE
job_id = $1;
-- name: UpdateTemplateVersionGitAuthProvidersByJobID :exec
UPDATE
template_versions
SET
git_auth_providers = $2,
updated_at = $3
WHERE
job_id = $1;
-- name: GetPreviousTemplateVersion :one
SELECT
*

View File

@ -34,14 +34,14 @@ type Config struct {
ValidateURL string
}
// ConvertConfig converts the YAML configuration entry to the
// parsed and ready-to-consume provider type.
// ConvertConfig converts the SDK configuration entry format
// to the parsed and ready-to-consume in coderd provider type.
func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Config, error) {
ids := map[string]struct{}{}
configs := []*Config{}
for _, entry := range entries {
var typ codersdk.GitProvider
switch entry.Type {
switch codersdk.GitProvider(entry.Type) {
case codersdk.GitProviderAzureDevops:
typ = codersdk.GitProviderAzureDevops
case codersdk.GitProviderBitBucket:

View File

@ -26,37 +26,37 @@ func TestConvertYAML(t *testing.T) {
}, {
Name: "InvalidID",
Input: []codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
ID: "$hi$",
}},
Error: "doesn't have a valid id",
}, {
Name: "NoClientID",
Input: []codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
}},
Error: "client_id must be provided",
}, {
Name: "NoClientSecret",
Input: []codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
ClientID: "example",
}},
Error: "client_secret must be provided",
}, {
Name: "DuplicateType",
Input: []codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
ClientID: "example",
ClientSecret: "example",
}, {
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
}},
Error: "multiple github git auth providers provided",
}, {
Name: "InvalidRegex",
Input: []codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitHub,
Type: string(codersdk.GitProviderGitHub),
ClientID: "example",
ClientSecret: "example",
Regex: `\K`,
@ -79,7 +79,7 @@ func TestConvertYAML(t *testing.T) {
t.Run("CustomScopesAndEndpoint", func(t *testing.T) {
t.Parallel()
config, err := gitauth.ConvertConfig([]codersdk.GitAuthConfig{{
Type: codersdk.GitProviderGitLab,
Type: string(codersdk.GitProviderGitLab),
ClientID: "id",
ClientSecret: "secret",
AuthURL: "https://auth.com",

View File

@ -29,6 +29,7 @@ import (
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/util/slice"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner"
"github.com/coder/coder/provisionerd/proto"
@ -42,16 +43,17 @@ var (
)
type Server struct {
AccessURL *url.URL
ID uuid.UUID
Logger slog.Logger
Provisioners []database.ProvisionerType
Tags json.RawMessage
Database database.Store
Pubsub database.Pubsub
Telemetry telemetry.Reporter
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
Auditor *atomic.Pointer[audit.Auditor]
AccessURL *url.URL
ID uuid.UUID
Logger slog.Logger
Provisioners []database.ProvisionerType
GitAuthProviders []string
Tags json.RawMessage
Database database.Store
Pubsub database.Pubsub
Telemetry telemetry.Reporter
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
Auditor *atomic.Pointer[audit.Auditor]
AcquireJobDebounce time.Duration
}
@ -307,7 +309,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
templateVersion, err := server.Database.GetTemplateVersionByID(ctx, templateVersionID)
if err != nil {
return nil, fmt.Errorf("get template version: %w", err)
return nil, xerrors.Errorf("get template version: %w", err)
}
if templateVersion.TemplateID.UUID == uuid.Nil {
@ -316,7 +318,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
template, err := server.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID)
if err != nil {
return nil, fmt.Errorf("get template: %w", err)
return nil, xerrors.Errorf("get template: %w", err)
}
if template.ActiveVersionID == uuid.Nil {
@ -325,7 +327,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
templateVariables, err := server.Database.GetTemplateVersionVariables(ctx, template.ActiveVersionID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("get template version variables: %w", err)
return nil, xerrors.Errorf("get template version variables: %w", err)
}
for _, templateVariable := range templateVariables {
@ -812,6 +814,27 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
}
}
var completedError sql.NullString
for _, gitAuthProvider := range jobType.TemplateImport.GitAuthProviders {
if !slice.Contains(server.GitAuthProviders, gitAuthProvider) {
completedError = sql.NullString{
String: fmt.Sprintf("git auth provider %q is not configured", gitAuthProvider),
Valid: true,
}
break
}
}
err = server.Database.UpdateTemplateVersionGitAuthProvidersByJobID(ctx, database.UpdateTemplateVersionGitAuthProvidersByJobIDParams{
JobID: jobID,
GitAuthProviders: jobType.TemplateImport.GitAuthProviders,
UpdatedAt: database.Now(),
})
if err != nil {
return nil, xerrors.Errorf("update template version git auth providers: %w", err)
}
err = server.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: jobID,
UpdatedAt: database.Now(),
@ -819,6 +842,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
Time: database.Now(),
Valid: true,
},
Error: completedError,
})
if err != nil {
return nil, xerrors.Errorf("update provisioner job: %w", err)

View File

@ -736,10 +736,16 @@ func TestCompleteJob(t *testing.T) {
t.Run("TemplateImport", func(t *testing.T) {
t.Parallel()
srv := setup(t, false)
jobID := uuid.New()
version, err := srv.Database.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
ID: uuid.New(),
JobID: jobID,
})
require.NoError(t, err)
job, err := srv.Database.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: uuid.New(),
ID: jobID,
Provisioner: database.ProvisionerTypeEcho,
Input: []byte(`{"template_version_id": "` + uuid.NewString() + `"}`),
Input: []byte(`{"template_version_id": "` + version.ID.String() + `"}`),
StorageMethod: database.ProvisionerStorageMethodFile,
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
@ -752,19 +758,31 @@ func TestCompleteJob(t *testing.T) {
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
})
require.NoError(t, err)
_, err = srv.CompleteJob(ctx, &proto.CompletedJob{
JobId: job.ID.String(),
Type: &proto.CompletedJob_TemplateImport_{
TemplateImport: &proto.CompletedJob_TemplateImport{
StartResources: []*sdkproto.Resource{{
Name: "hello",
Type: "aws_instance",
}},
StopResources: []*sdkproto.Resource{},
completeJob := func() {
_, err = srv.CompleteJob(ctx, &proto.CompletedJob{
JobId: job.ID.String(),
Type: &proto.CompletedJob_TemplateImport_{
TemplateImport: &proto.CompletedJob_TemplateImport{
StartResources: []*sdkproto.Resource{{
Name: "hello",
Type: "aws_instance",
}},
StopResources: []*sdkproto.Resource{},
GitAuthProviders: []string{"github"},
},
},
},
})
})
require.NoError(t, err)
}
completeJob()
job, err = srv.Database.GetProvisionerJobByID(ctx, job.ID)
require.NoError(t, err)
require.Contains(t, job.Error.String, `git auth provider "github" is not configured`)
srv.GitAuthProviders = []string{"github"}
completeJob()
job, err = srv.Database.GetProvisionerJobByID(ctx, job.ID)
require.NoError(t, err)
require.False(t, job.Error.Valid)
})
t.Run("WorkspaceBuild", func(t *testing.T) {
t.Parallel()

View File

@ -19,6 +19,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/parameter"
@ -243,6 +244,107 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re
httpapi.Write(ctx, rw, http.StatusOK, templateVersionParameters)
}
// @Summary Get git auth by template version
// @ID get-git-auth-by-template-version
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 200 {array} codersdk.TemplateVersionGitAuth
// @Router /templateversions/{templateversion}/gitauth [get]
func (api *API) templateVersionGitAuth(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var (
apiKey = httpmw.APIKey(r)
templateVersion = httpmw.TemplateVersionParam(r)
template = httpmw.TemplateParam(r)
)
if !api.Authorize(r, rbac.ActionRead, templateVersion.RBACObject(template)) {
httpapi.ResourceNotFound(rw)
return
}
rawProviders := templateVersion.GitAuthProviders
providers := make([]codersdk.TemplateVersionGitAuth, 0)
for _, rawProvider := range rawProviders {
var config *gitauth.Config
for _, provider := range api.GitAuthConfigs {
if provider.ID == rawProvider {
config = provider
break
}
}
if config == nil {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("The template version references a Git auth provider %q that no longer exists.", rawProvider),
Detail: "You'll need to update the template version to use a different provider.",
})
return
}
// This is the URL that will redirect the user with a state token.
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/gitauth/%s", config.ID))
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to parse access URL.",
Detail: err.Error(),
})
return
}
query := redirectURL.Query()
// The frontend uses a BroadcastChannel to notify listening pages for
// Git auth updates if the "notify" query parameter is set.
//
// It's important we do this in the backend, because the same endpoint
// is used for CLI authentication.
query.Add("redirect", "/gitauth?notify")
redirectURL.RawQuery = query.Encode()
provider := codersdk.TemplateVersionGitAuth{
ID: config.ID,
Type: config.Type,
AuthenticateURL: redirectURL.String(),
}
authLink, err := api.Database.GetGitAuthLink(ctx, database.GetGitAuthLinkParams{
ProviderID: config.ID,
UserID: apiKey.UserID,
})
// If there isn't an auth link, then the user just isn't authenticated.
if errors.Is(err, sql.ErrNoRows) {
providers = append(providers, provider)
continue
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching git auth link.",
Detail: err.Error(),
})
return
}
_, updated, err := refreshGitToken(ctx, api.Database, apiKey.UserID, config, authLink)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to refresh git auth token.",
Detail: err.Error(),
})
return
}
// If the token couldn't be validated, then we assume the user isn't
// authenticated and return early.
if !updated {
providers = append(providers, provider)
continue
}
provider.Authenticated = true
providers = append(providers, provider)
}
httpapi.Write(ctx, rw, http.StatusOK, providers)
}
// @Summary Get template variables by template version
// @ID get-template-variables-by-template-version
// @Security CoderSessionToken

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"net/http"
"regexp"
"testing"
"github.com/google/uuid"
@ -14,6 +15,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/examples"
@ -434,6 +436,67 @@ func TestTemplateVersionParameters(t *testing.T) {
})
}
func TestTemplateVersionsGitAuth(t *testing.T) {
t.Parallel()
t.Run("Empty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.TemplateVersionGitAuth(ctx, version.ID)
require.NoError(t, err)
})
t.Run("Authenticated", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
GitAuthConfigs: []*gitauth.Config{{
OAuth2Config: &oauth2Config{},
ID: "github",
Regex: regexp.MustCompile(`github\.com`),
Type: codersdk.GitProviderGitHub,
}},
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
GitAuthProviders: []string{"github"},
},
},
}},
})
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Empty(t, version.Job.Error)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Not authenticated to start!
providers, err := client.TemplateVersionGitAuth(ctx, version.ID)
require.NoError(t, err)
require.Len(t, providers, 1)
require.False(t, providers[0].Authenticated)
// Perform the Git auth callback to authenticate the user...
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
_ = resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
// Ensure that the returned Git auth for the template is authenticated!
providers, err = client.TemplateVersionGitAuth(ctx, version.ID)
require.NoError(t, err)
require.Len(t, providers, 1)
require.True(t, providers[0].Authenticated)
})
}
func TestTemplateVersionResources(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {

View File

@ -1313,14 +1313,29 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
return
}
// If the token is expired and refresh is disabled, we prompt
// the user to authenticate again.
if gitAuthConfig.NoRefresh && gitAuthLink.OAuthExpiry.Before(database.Now()) {
gitAuthLink, updated, err := refreshGitToken(ctx, api.Database, workspace.OwnerID, gitAuthConfig, gitAuthLink)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to refresh git auth token.",
Detail: err.Error(),
})
return
}
if !updated {
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{
URL: redirectURL.String(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(gitAuthConfig.Type, gitAuthLink.OAuthAccessToken))
}
func refreshGitToken(ctx context.Context, db database.Store, owner uuid.UUID, gitAuthConfig *gitauth.Config, gitAuthLink database.GitAuthLink) (database.GitAuthLink, bool, error) {
// If the token is expired and refresh is disabled, we prompt
// the user to authenticate again.
if gitAuthConfig.NoRefresh && gitAuthLink.OAuthExpiry.Before(database.Now()) {
return gitAuthLink, false, nil
}
token, err := gitAuthConfig.TokenSource(ctx, &oauth2.Token{
AccessToken: gitAuthLink.OAuthAccessToken,
@ -1328,49 +1343,35 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
Expiry: gitAuthLink.OAuthExpiry,
}).Token()
if err != nil {
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{
URL: redirectURL.String(),
})
return
return gitAuthLink, false, nil
}
if gitAuthConfig.ValidateURL != "" {
valid, err := validateGitToken(ctx, gitAuthConfig.ValidateURL, token.AccessToken)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate Git authentication token.",
Detail: err.Error(),
})
return
return gitAuthLink, false, xerrors.Errorf("validate git auth token: %w", err)
}
if !valid {
// The token is no longer valid!
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{
URL: redirectURL.String(),
})
return
return gitAuthLink, false, nil
}
}
if token.AccessToken != gitAuthLink.OAuthAccessToken {
// Update it
err = api.Database.UpdateGitAuthLink(ctx, database.UpdateGitAuthLinkParams{
gitAuthLink, err = db.UpdateGitAuthLink(ctx, database.UpdateGitAuthLinkParams{
ProviderID: gitAuthConfig.ID,
UserID: workspace.OwnerID,
UserID: owner,
UpdatedAt: database.Now(),
OAuthAccessToken: token.AccessToken,
OAuthRefreshToken: token.RefreshToken,
OAuthExpiry: token.Expiry,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update git auth link.",
Detail: err.Error(),
})
return
return gitAuthLink, false, xerrors.Errorf("update git auth link: %w", err)
}
}
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(gitAuthConfig.Type, token.AccessToken))
return gitAuthLink, true, nil
}
// validateGitToken ensures the git token provided is valid
@ -1392,7 +1393,7 @@ func validateGitToken(ctx context.Context, validateURL, token string) (bool, err
}
if res.StatusCode != http.StatusOK {
data, _ := io.ReadAll(res.Body)
return false, xerrors.Errorf("git token validation failed: status %d: body: %s", res.StatusCode, data)
return false, xerrors.Errorf("status %d: body: %s", res.StatusCode, data)
}
return true, nil
}
@ -1459,7 +1460,7 @@ func (api *API) gitAuthCallback(gitAuthConfig *gitauth.Config) http.HandlerFunc
return
}
} else {
err = api.Database.UpdateGitAuthLink(ctx, database.UpdateGitAuthLinkParams{
_, err = api.Database.UpdateGitAuthLink(ctx, database.UpdateGitAuthLinkParams{
ProviderID: gitAuthConfig.ID,
UserID: apiKey.UserID,
UpdatedAt: database.Now(),
@ -1485,8 +1486,12 @@ func (api *API) gitAuthCallback(gitAuthConfig *gitauth.Config) http.HandlerFunc
return
}
redirect := state.Redirect
if redirect == "" {
redirect = "/gitauth"
}
// This is a nicely rendered screen on the frontend
http.Redirect(rw, r, "/gitauth", http.StatusTemporaryRedirect)
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
}
}

View File

@ -911,7 +911,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
Type: codersdk.GitProviderGitHub,
}},
})
resp := gitAuthCallback(t, "github", client)
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("AuthorizedCallback", func(t *testing.T) {
@ -926,14 +926,14 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
}},
})
_ = coderdtest.CreateFirstUser(t, client)
resp := gitAuthCallback(t, "github", client)
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
location, err := resp.Location()
require.NoError(t, err)
require.Equal(t, "/gitauth", location.Path)
// Callback again to simulate updating the token.
resp = gitAuthCallback(t, "github", client)
resp = coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
})
t.Run("ValidateURL", func(t *testing.T) {
@ -983,7 +983,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
resp := gitAuthCallback(t, "github", client)
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
// If the validation URL says unauthorized, the callback
@ -1005,7 +1005,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusInternalServerError, apiError.StatusCode())
require.Equal(t, "git token validation failed: status 403: body: Something went wrong!", apiError.Detail)
require.Equal(t, "validate git auth token: status 403: body: Something went wrong!", apiError.Detail)
})
t.Run("ExpiredNoRefresh", func(t *testing.T) {
@ -1063,7 +1063,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
// In the configuration, we set our OAuth provider
// to return an expired token. Coder consumes this
// and stores it.
resp := gitAuthCallback(t, "github", client)
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
// Because the token is expired and `NoRefresh` is specified,
@ -1128,7 +1128,7 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
time.Sleep(250 * time.Millisecond)
resp := gitAuthCallback(t, "github", client)
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
token = <-tokenChan
require.Equal(t, "token", token.Username)
@ -1197,31 +1197,6 @@ func TestWorkspaceAgentReportStats(t *testing.T) {
})
}
func gitAuthCallback(t *testing.T, id string, client *codersdk.Client) *http.Response {
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
state := "somestate"
oauthURL, err := client.URL.Parse(fmt.Sprintf("/gitauth/%s/callback?code=asd&state=%s", id, state))
require.NoError(t, err)
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: codersdk.OAuth2StateCookie,
Value: state,
})
req.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenCookie,
Value: client.SessionToken(),
})
res, err := client.HTTPClient.Do(req)
require.NoError(t, err)
t.Cleanup(func() {
_ = res.Body.Close()
})
return res
}
func TestWorkspaceAgent_LifecycleState(t *testing.T) {
t.Parallel()

View File

@ -24,6 +24,13 @@ type TemplateVersion struct {
CreatedBy User `json:"created_by"`
}
type TemplateVersionGitAuth struct {
ID string `json:"id"`
Type GitProvider `json:"type"`
AuthenticateURL string `json:"authenticate_url"`
Authenticated bool `json:"authenticated"`
}
type ValidationMonotonicOrder string
const (
@ -108,6 +115,20 @@ func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid
return params, json.NewDecoder(res.Body).Decode(&params)
}
// TemplateVersionGitAuth returns git authentication for the requested template version.
func (c *Client) TemplateVersionGitAuth(ctx context.Context, version uuid.UUID) ([]TemplateVersionGitAuth, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/gitauth", version), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var gitAuth []TemplateVersionGitAuth
return gitAuth, json.NewDecoder(res.Body).Decode(&gitAuth)
}
// TemplateVersionSchema returns schemas for a template version by ID.
func (c *Client) TemplateVersionSchema(ctx context.Context, version uuid.UUID) ([]ParameterSchema, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/schema", version), nil)

View File

@ -291,12 +291,26 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.
// GitProvider is a constant that represents the
// type of providers that are supported within Coder.
// @typescript-ignore GitProvider
type GitProvider string
func (g GitProvider) Pretty() string {
switch g {
case GitProviderAzureDevops:
return "Azure DevOps"
case GitProviderGitHub:
return "GitHub"
case GitProviderGitLab:
return "GitLab"
case GitProviderBitBucket:
return "Bitbucket"
default:
return string(g)
}
}
const (
GitProviderAzureDevops = "azure-devops"
GitProviderGitHub = "github"
GitProviderGitLab = "gitlab"
GitProviderBitBucket = "bitbucket"
GitProviderAzureDevops GitProvider = "azure-devops"
GitProviderGitHub GitProvider = "github"
GitProviderGitLab GitProvider = "gitlab"
GitProviderBitBucket GitProvider = "bitbucket"
)

View File

@ -16,7 +16,7 @@ We track the following resources:
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_private</td><td>true</td></tr><tr><td>min_autostart_interval</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |

View File

@ -3020,6 +3020,23 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| `type` | string | false | | |
| `validate_url` | string | false | | |
## codersdk.GitProvider
```json
"azure-devops"
```
### Properties
#### Enumerated Values
| Value |
| -------------- |
| `azure-devops` |
| `github` |
| `gitlab` |
| `bitbucket` |
## codersdk.GitSSHKey
```json
@ -4700,6 +4717,26 @@ Parameter represents a set value for the scope.
| `template_id` | string | false | | |
| `updated_at` | string | false | | |
## codersdk.TemplateVersionGitAuth
```json
{
"authenticate_url": "string",
"authenticated": true,
"id": "string",
"type": "azure-devops"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------ | -------------------------------------------- | -------- | ------------ | ----------- |
| `authenticate_url` | string | false | | |
| `authenticated` | boolean | false | | |
| `id` | string | false | | |
| `type` | [codersdk.GitProvider](#codersdkgitprovider) | false | | |
## codersdk.TemplateVersionParameter
```json

View File

@ -1722,6 +1722,69 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get git auth by template version
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/gitauth \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /templateversions/{templateversion}/gitauth`
### Parameters
| Name | In | Type | Required | Description |
| ----------------- | ---- | ------------ | -------- | ------------------- |
| `templateversion` | path | string(uuid) | true | Template version ID |
### Example responses
> 200 Response
```json
[
{
"authenticate_url": "string",
"authenticated": true,
"id": "string",
"type": "azure-devops"
}
]
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.TemplateVersionGitAuth](schemas.md#codersdktemplateversiongitauth) |
<h3 id="get-git-auth-by-template-version-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| -------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- |
| `[array item]` | array | false | | |
| `» authenticate_url` | string | false | | |
| `» authenticated` | boolean | false | | |
| `» id` | string | false | | |
| `» type` | [codersdk.GitProvider](schemas.md#codersdkgitprovider) | false | | |
#### Enumerated Values
| Property | Value |
| -------- | -------------- |
| `type` | `azure-devops` |
| `type` | `github` |
| `type` | `gitlab` |
| `type` | `bitbucket` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get logs by template version
### Code samples

View File

@ -72,15 +72,16 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"allow_user_cancel_workspace_jobs": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
"template_id": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"name": ActionTrack,
"readme": ActionTrack,
"job_id": ActionIgnore, // Not helpful in a diff because jobs aren't tracked in audit logs.
"created_by": ActionTrack,
"id": ActionTrack,
"template_id": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"name": ActionTrack,
"readme": ActionTrack,
"job_id": ActionIgnore, // Not helpful in a diff because jobs aren't tracked in audit logs.
"created_by": ActionTrack,
"git_auth_providers": ActionIgnore, // Not helpful because this can only change when new versions are added.
},
&database.User{}: {
"id": ActionTrack,

View File

@ -223,7 +223,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
if err != nil {
return nil, xerrors.Errorf("terraform plan: %w", err)
}
resources, parameters, err := e.planResources(ctx, killCtx, planfilePath)
state, err := e.planResources(ctx, killCtx, planfilePath)
if err != nil {
return nil, err
}
@ -234,31 +234,32 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: parameters,
Resources: resources,
Plan: planFileByt,
Parameters: state.Parameters,
Resources: state.Resources,
GitAuthProviders: state.GitAuthProviders,
Plan: planFileByt,
},
},
}, nil
}
// planResources must only be called while the lock is held.
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) ([]*proto.Resource, []*proto.RichParameter, error) {
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, error) {
plan, err := e.showPlan(ctx, killCtx, planfilePath)
if err != nil {
return nil, nil, xerrors.Errorf("show terraform plan file: %w", err)
return nil, xerrors.Errorf("show terraform plan file: %w", err)
}
rawGraph, err := e.graph(ctx, killCtx)
if err != nil {
return nil, nil, xerrors.Errorf("graph: %w", err)
return nil, xerrors.Errorf("graph: %w", err)
}
modules := []*tfjson.StateModule{}
if plan.PriorState != nil {
modules = append(modules, plan.PriorState.Values.RootModule)
}
modules = append(modules, plan.PlannedValues.RootModule)
return ConvertResourcesAndParameters(modules, rawGraph)
return ConvertState(modules, rawGraph)
}
// showPlan must only be called while the lock is held.
@ -332,7 +333,7 @@ func (e *executor) apply(
if err != nil {
return nil, xerrors.Errorf("terraform apply: %w", err)
}
resources, parameters, err := e.stateResources(ctx, killCtx)
state, err := e.stateResources(ctx, killCtx)
if err != nil {
return nil, err
}
@ -344,35 +345,35 @@ func (e *executor) apply(
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: parameters,
Resources: resources,
State: stateContent,
Parameters: state.Parameters,
Resources: state.Resources,
GitAuthProviders: state.GitAuthProviders,
State: stateContent,
},
},
}, nil
}
// stateResources must only be called while the lock is held.
func (e *executor) stateResources(ctx, killCtx context.Context) ([]*proto.Resource, []*proto.RichParameter, error) {
func (e *executor) stateResources(ctx, killCtx context.Context) (*State, error) {
state, err := e.state(ctx, killCtx)
if err != nil {
return nil, nil, err
return nil, err
}
rawGraph, err := e.graph(ctx, killCtx)
if err != nil {
return nil, nil, xerrors.Errorf("get terraform graph: %w", err)
return nil, xerrors.Errorf("get terraform graph: %w", err)
}
var resources []*proto.Resource
var parameters []*proto.RichParameter
converted := &State{}
if state.Values != nil {
resources, parameters, err = ConvertResourcesAndParameters([]*tfjson.StateModule{
converted, err = ConvertState([]*tfjson.StateModule{
state.Values.RootModule,
}, rawGraph)
if err != nil {
return nil, nil, err
return nil, err
}
}
return resources, parameters, nil
return converted, nil
}
// state must only be called while the lock is held.

View File

@ -72,17 +72,23 @@ type metadataItem struct {
IsNull bool `mapstructure:"is_null"`
}
// ConvertResourcesAndParameters consumes Terraform state and a GraphViz representation
type State struct {
Resources []*proto.Resource
Parameters []*proto.RichParameter
GitAuthProviders []string
}
// ConvertState consumes Terraform state and a GraphViz representation
// produced by `terraform graph` to produce resources consumable by Coder.
// nolint:gocyclo
func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph string) ([]*proto.Resource, []*proto.RichParameter, error) {
func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) {
parsedGraph, err := gographviz.ParseString(rawGraph)
if err != nil {
return nil, nil, xerrors.Errorf("parse graph: %w", err)
return nil, xerrors.Errorf("parse graph: %w", err)
}
graph, err := gographviz.NewAnalysedGraph(parsedGraph)
if err != nil {
return nil, nil, xerrors.Errorf("analyze graph: %w", err)
return nil, xerrors.Errorf("analyze graph: %w", err)
}
resources := make([]*proto.Resource, 0)
@ -118,11 +124,11 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
var attrs agentAttributes
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode agent attributes: %w", err)
return nil, xerrors.Errorf("decode agent attributes: %w", err)
}
if _, ok := agentNames[tfResource.Name]; ok {
return nil, nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
return nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
}
agentNames[tfResource.Name] = struct{}{}
@ -171,7 +177,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
break
}
if agentNode == nil {
return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
return nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
}
var agentResource *graphResource
@ -260,7 +266,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
var attrs agentAppAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode app attributes: %w", err)
return nil, xerrors.Errorf("decode app attributes: %w", err)
}
// Default to the resource name if none is set!
@ -277,11 +283,11 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
}
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
return nil, nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
return nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
}
if _, exists := appSlugs[attrs.Slug]; exists {
return nil, nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
return nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
}
appSlugs[attrs.Slug] = struct{}{}
@ -341,7 +347,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
var attrs metadataAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode metadata attributes: %w", err)
return nil, xerrors.Errorf("decode metadata attributes: %w", err)
}
resourceLabel := convertAddressToLabel(resource.Address)
@ -432,7 +438,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
var param provider.Parameter
err = mapstructure.Decode(resource.AttributeValues, &param)
if err != nil {
return nil, nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
return nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
}
protoParam := &proto.RichParameter{
Name: param.Name,
@ -464,7 +470,30 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
}
}
return resources, parameters, nil
// A map is used to ensure we don't have duplicates!
gitAuthProvidersMap := map[string]struct{}{}
for _, tfResources := range tfResourcesByLabel {
for _, resource := range tfResources {
if resource.Type != "coder_git_auth" {
continue
}
id, ok := resource.AttributeValues["id"].(string)
if !ok {
return nil, xerrors.Errorf("git auth id is not a string")
}
gitAuthProvidersMap[id] = struct{}{}
}
}
gitAuthProviders := make([]string, 0, len(gitAuthProvidersMap))
for id := range gitAuthProvidersMap {
gitAuthProviders = append(gitAuthProviders, id)
}
return &State{
Resources: resources,
Parameters: parameters,
GitAuthProviders: gitAuthProviders,
}, nil
}
// convertAddressToLabel returns the Terraform address without the count

View File

@ -22,8 +22,9 @@ func TestConvertResources(t *testing.T) {
// nolint:dogsled
_, filename, _, _ := runtime.Caller(0)
type testCase struct {
resources []*proto.Resource
parameters []*proto.RichParameter
resources []*proto.Resource
parameters []*proto.RichParameter
gitAuthProviders []string
}
// nolint:paralleltest
for folderName, expected := range map[string]testCase{
@ -294,6 +295,22 @@ func TestConvertResources(t *testing.T) {
}},
}},
},
"git-auth-providers": {
resources: []*proto.Resource{{
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
LoginBeforeReady: true,
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
}},
}},
gitAuthProviders: []string{"github", "gitlab"},
},
} {
folderName := folderName
expected := expected
@ -319,10 +336,11 @@ func TestConvertResources(t *testing.T) {
// and that no errors occur!
modules = append(modules, tfPlan.PlannedValues.RootModule)
}
resources, parameters, err := terraform.ConvertResourcesAndParameters(modules, string(tfPlanGraph))
state, err := terraform.ConvertState(modules, string(tfPlanGraph))
require.NoError(t, err)
sortResources(resources)
sortParameters(parameters)
sortResources(state.Resources)
sortParameters(state.Parameters)
sort.Strings(state.GitAuthProviders)
expectedNoMetadata := make([]*proto.Resource, 0)
for _, resource := range expected.resources {
@ -342,7 +360,7 @@ func TestConvertResources(t *testing.T) {
err = json.Unmarshal(data, &expectedNoMetadataMap)
require.NoError(t, err)
data, err = json.Marshal(resources)
data, err = json.Marshal(state.Resources)
require.NoError(t, err)
var resourcesMap []map[string]interface{}
err = json.Unmarshal(data, &resourcesMap)
@ -354,10 +372,12 @@ func TestConvertResources(t *testing.T) {
}
parametersWant, err := json.Marshal(expected.parameters)
require.NoError(t, err)
parametersGot, err := json.Marshal(parameters)
parametersGot, err := json.Marshal(state.Parameters)
require.NoError(t, err)
require.Equal(t, string(parametersWant), string(parametersGot))
require.Equal(t, expectedNoMetadataMap, resourcesMap)
require.ElementsMatch(t, expected.gitAuthProviders, state.GitAuthProviders)
})
t.Run("Provision", func(t *testing.T) {
@ -370,11 +390,12 @@ func TestConvertResources(t *testing.T) {
tfStateGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.dot"))
require.NoError(t, err)
resources, parameters, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph))
state, err := terraform.ConvertState([]*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph))
require.NoError(t, err)
sortResources(resources)
sortParameters(parameters)
for _, resource := range resources {
sortResources(state.Resources)
sortParameters(state.Parameters)
sort.Strings(state.GitAuthProviders)
for _, resource := range state.Resources {
for _, agent := range resource.Agents {
agent.Id = ""
if agent.GetToken() != "" {
@ -393,13 +414,14 @@ func TestConvertResources(t *testing.T) {
err = json.Unmarshal(data, &expectedMap)
require.NoError(t, err)
data, err = json.Marshal(resources)
data, err = json.Marshal(state.Resources)
require.NoError(t, err)
var resourcesMap []map[string]interface{}
err = json.Unmarshal(data, &resourcesMap)
require.NoError(t, err)
require.Equal(t, expectedMap, resourcesMap)
require.ElementsMatch(t, expected.gitAuthProviders, state.GitAuthProviders)
})
})
}
@ -428,8 +450,8 @@ func TestAppSlugValidation(t *testing.T) {
}
}
resources, _, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
require.Nil(t, resources)
state, err := terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
require.Nil(t, state)
require.Error(t, err)
require.ErrorContains(t, err, "invalid app slug")
@ -440,8 +462,8 @@ func TestAppSlugValidation(t *testing.T) {
}
}
resources, _, err = terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
require.Nil(t, resources)
state, err = terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
require.Nil(t, state)
require.Error(t, err)
require.ErrorContains(t, err, "duplicate app slug")
}
@ -473,7 +495,7 @@ func TestInstanceTypeAssociation(t *testing.T) {
t.Parallel()
instanceType, err := cryptorand.String(12)
require.NoError(t, err)
resources, _, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{{
state, err := terraform.ConvertState([]*tfjson.StateModule{{
Resources: []*tfjson.StateResource{{
Address: tc.ResourceType + ".dev",
Type: tc.ResourceType,
@ -492,8 +514,8 @@ func TestInstanceTypeAssociation(t *testing.T) {
}
}`)
require.NoError(t, err)
require.Len(t, resources, 1)
require.Equal(t, resources[0].GetInstanceType(), instanceType)
require.Len(t, state.Resources, 1)
require.Equal(t, state.Resources[0].GetInstanceType(), instanceType)
})
}
}
@ -531,7 +553,7 @@ func TestInstanceIDAssociation(t *testing.T) {
t.Parallel()
instanceID, err := cryptorand.String(12)
require.NoError(t, err)
resources, _, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{{
state, err := terraform.ConvertState([]*tfjson.StateModule{{
Resources: []*tfjson.StateResource{{
Address: "coder_agent.dev",
Type: "coder_agent",
@ -563,9 +585,9 @@ func TestInstanceIDAssociation(t *testing.T) {
}
`)
require.NoError(t, err)
require.Len(t, resources, 1)
require.Len(t, resources[0].Agents, 1)
require.Equal(t, resources[0].Agents[0].GetInstanceId(), instanceID)
require.Len(t, state.Resources, 1)
require.Len(t, state.Resources[0].Agents, 1)
require.Equal(t, state.Resources[0].Agents[0].GetInstanceId(), instanceID)
})
}
}

View File

@ -0,0 +1,27 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.6.13"
}
}
}
data "coder_git_auth" "github" {
id = "github"
}
data "coder_git_auth" "gitlab" {
id = "gitlab"
}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
}
resource "null_resource" "dev" {
depends_on = [
coder_agent.main
]
}

View File

@ -0,0 +1,24 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"]
"[root] data.coder_git_auth.github (expand)" [label = "data.coder_git_auth.github", shape = "box"]
"[root] data.coder_git_auth.gitlab (expand)" [label = "data.coder_git_auth.gitlab", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] data.coder_git_auth.github (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] data.coder_git_auth.gitlab (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.github (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.gitlab (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}

View File

@ -0,0 +1,212 @@
{
"format_version": "1.1",
"terraform_version": "1.3.7",
"planned_values": {
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"login_before_ready": true,
"motd_file": null,
"os": "linux",
"shutdown_script": null,
"shutdown_script_timeout": 300,
"startup_script": null,
"startup_script_timeout": 300,
"troubleshooting_url": null
},
"sensitive_values": {}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"triggers": null
},
"sensitive_values": {}
}
]
}
},
"resource_changes": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"login_before_ready": true,
"motd_file": null,
"os": "linux",
"shutdown_script": null,
"shutdown_script_timeout": 300,
"startup_script": null,
"startup_script_timeout": 300,
"troubleshooting_url": null
},
"after_unknown": {
"id": true,
"init_script": true,
"token": true
},
"before_sensitive": false,
"after_sensitive": {
"token": true
}
}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"triggers": null
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
}
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.3.7",
"values": {
"root_module": {
"resources": [
{
"address": "data.coder_git_auth.github",
"mode": "data",
"type": "coder_git_auth",
"name": "github",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"access_token": "",
"id": "github"
},
"sensitive_values": {}
},
{
"address": "data.coder_git_auth.gitlab",
"mode": "data",
"type": "coder_git_auth",
"name": "gitlab",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"access_token": "",
"id": "gitlab"
},
"sensitive_values": {}
}
]
}
}
},
"configuration": {
"provider_config": {
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.6.13"
},
"null": {
"name": "null",
"full_name": "registry.terraform.io/hashicorp/null"
}
},
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_config_key": "coder",
"expressions": {
"arch": {
"constant_value": "amd64"
},
"os": {
"constant_value": "linux"
}
},
"schema_version": 0
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_config_key": "null",
"schema_version": 0,
"depends_on": [
"coder_agent.main"
]
},
{
"address": "data.coder_git_auth.github",
"mode": "data",
"type": "coder_git_auth",
"name": "github",
"provider_config_key": "coder",
"expressions": {
"id": {
"constant_value": "github"
}
},
"schema_version": 0
},
{
"address": "data.coder_git_auth.gitlab",
"mode": "data",
"type": "coder_git_auth",
"name": "gitlab",
"provider_config_key": "coder",
"expressions": {
"id": {
"constant_value": "gitlab"
}
},
"schema_version": 0
}
]
}
}
}

View File

@ -0,0 +1,24 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"]
"[root] data.coder_git_auth.github (expand)" [label = "data.coder_git_auth.github", shape = "box"]
"[root] data.coder_git_auth.gitlab (expand)" [label = "data.coder_git_auth.gitlab", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] data.coder_git_auth.github (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] data.coder_git_auth.gitlab (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.github (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.gitlab (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}

View File

@ -0,0 +1,79 @@
{
"format_version": "1.0",
"terraform_version": "1.3.7",
"values": {
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"id": "78b29f93-097d-403b-ab56-0bc943d427cc",
"init_script": "",
"login_before_ready": true,
"motd_file": null,
"os": "linux",
"shutdown_script": null,
"shutdown_script_timeout": 300,
"startup_script": null,
"startup_script_timeout": 300,
"token": "a57838e5-355c-471a-9a85-f81314fbaec6",
"troubleshooting_url": null
},
"sensitive_values": {}
},
{
"address": "data.coder_git_auth.github",
"mode": "data",
"type": "coder_git_auth",
"name": "github",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"access_token": "",
"id": "github"
},
"sensitive_values": {}
},
{
"address": "data.coder_git_auth.gitlab",
"mode": "data",
"type": "coder_git_auth",
"name": "gitlab",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"access_token": "",
"id": "gitlab"
},
"sensitive_values": {}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "1416347524569828366",
"triggers": null
},
"sensitive_values": {},
"depends_on": [
"coder_agent.main"
]
}
]
}
}
}

View File

@ -1213,9 +1213,10 @@ type CompletedJob_TemplateImport struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
StartResources []*proto.Resource `protobuf:"bytes,1,rep,name=start_resources,json=startResources,proto3" json:"start_resources,omitempty"`
StopResources []*proto.Resource `protobuf:"bytes,2,rep,name=stop_resources,json=stopResources,proto3" json:"stop_resources,omitempty"`
RichParameters []*proto.RichParameter `protobuf:"bytes,3,rep,name=rich_parameters,json=richParameters,proto3" json:"rich_parameters,omitempty"`
StartResources []*proto.Resource `protobuf:"bytes,1,rep,name=start_resources,json=startResources,proto3" json:"start_resources,omitempty"`
StopResources []*proto.Resource `protobuf:"bytes,2,rep,name=stop_resources,json=stopResources,proto3" json:"stop_resources,omitempty"`
RichParameters []*proto.RichParameter `protobuf:"bytes,3,rep,name=rich_parameters,json=richParameters,proto3" json:"rich_parameters,omitempty"`
GitAuthProviders []string `protobuf:"bytes,4,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
}
func (x *CompletedJob_TemplateImport) Reset() {
@ -1271,6 +1272,13 @@ func (x *CompletedJob_TemplateImport) GetRichParameters() []*proto.RichParameter
return nil
}
func (x *CompletedJob_TemplateImport) GetGitAuthProviders() []string {
if x != nil {
return x.GitAuthProviders
}
return nil
}
type CompletedJob_TemplateDryRun struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -1435,7 +1443,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x73, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c,
0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x22, 0xaa, 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a,
0x65, 0x22, 0xd8, 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a,
0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72,
0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01,
@ -1459,7 +1467,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x74, 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0xd3, 0x01, 0x0a, 0x0e, 0x54, 0x65,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x81, 0x02, 0x0a, 0x0e, 0x54, 0x65,
0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f,
0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
@ -1472,98 +1480,100 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52,
0x0e, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x1a,
0x45, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75,
0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0,
0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52,
0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c,
0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74,
0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75,
0x74, 0x22, 0xcf, 0x02, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52,
0x0e, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12,
0x2c, 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76,
0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74,
0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x45, 0x0a,
0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12,
0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, 0x01, 0x0a,
0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76,
0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75,
0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22,
0xcf, 0x02, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c,
0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76,
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f,
0x67, 0x73, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f,
0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61,
0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72,
0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x4c, 0x0a,
0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62,
0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76,
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65,
0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61,
0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76,
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61,
0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61,
0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d,
0x65, 0x22, 0xbc, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65,
0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65,
0x6c, 0x65, 0x64, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72,
0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61,
0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61,
0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76,
0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a,
0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72,
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04,
0x6c, 0x6f, 0x67, 0x73, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70,
0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12,
0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69,
0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72,
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61,
0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70,
0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a,
0x14, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72,
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62,
0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72,
0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72,
0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61,
0x64, 0x6d, 0x65, 0x22, 0xbc, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f,
0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6e,
0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x61, 0x6e,
0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74,
0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61,
0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a,
0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c,
0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74,
0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12,
0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x22, 0x68,
0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73,
0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64,
0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05,
0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49,
0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a,
0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32, 0xec,
0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61,
0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a,
0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a,
0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74,
0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12,
0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46,
0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a,
0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70,
0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2b, 0x5a,
0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65,
0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a,
0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x22, 0x68, 0x0a, 0x13,
0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x63,
0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63,
0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x12, 0x16,
0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06,
0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e,
0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50,
0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32, 0xec, 0x02, 0x0a,
0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d,
0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62,
0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62,
0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x12,
0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43,
0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f,
0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, 0x2e,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69,
0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43,
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2b, 0x5a, 0x29, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f,
0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -69,6 +69,7 @@ message CompletedJob {
repeated provisioner.Resource start_resources = 1;
repeated provisioner.Resource stop_resources = 2;
repeated provisioner.RichParameter rich_parameters = 3;
repeated string git_auth_providers = 4;
}
message TemplateDryRun {
repeated provisioner.Resource resources = 1;

View File

@ -172,6 +172,11 @@ func (p *Server) connect(ctx context.Context) {
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
// It's possible for the provisioner daemon to be shut down
// before the wait is complete!
if p.isClosed() {
return
}
client, err := p.clientDialer(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {

View File

@ -600,7 +600,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
Stage: "Detecting persistent resources",
CreatedAt: time.Now().UnixMilli(),
})
startResources, parameters, err := r.runTemplateImportProvision(ctx, updateResponse.ParameterValues, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
startProvision, err := r.runTemplateImportProvision(ctx, updateResponse.ParameterValues, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
})
@ -615,7 +615,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
Stage: "Detecting ephemeral resources",
CreatedAt: time.Now().UnixMilli(),
})
stopResources, _, err := r.runTemplateImportProvision(ctx, updateResponse.ParameterValues, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
stopProvision, err := r.runTemplateImportProvision(ctx, updateResponse.ParameterValues, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceTransition: sdkproto.WorkspaceTransition_STOP,
})
@ -627,9 +627,10 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
JobId: r.job.JobId,
Type: &proto.CompletedJob_TemplateImport_{
TemplateImport: &proto.CompletedJob_TemplateImport{
StartResources: startResources,
StopResources: stopResources,
RichParameters: parameters,
StartResources: startProvision.Resources,
StopResources: stopProvision.Resources,
RichParameters: startProvision.Parameters,
GitAuthProviders: startProvision.GitAuthProviders,
},
},
}, nil
@ -680,16 +681,22 @@ func (r *Runner) runTemplateImportParse(ctx context.Context) ([]*sdkproto.Parame
}
}
type templateImportProvision struct {
Resources []*sdkproto.Resource
Parameters []*sdkproto.RichParameter
GitAuthProviders []string
}
// Performs a dry-run provision when importing a template.
// This is used to detect resources that would be provisioned for a workspace in various states.
// It doesn't define values for rich parameters as they're unknown during template import.
func (r *Runner) runTemplateImportProvision(ctx context.Context, values []*sdkproto.ParameterValue, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Provision_Metadata) ([]*sdkproto.Resource, []*sdkproto.RichParameter, error) {
func (r *Runner) runTemplateImportProvision(ctx context.Context, values []*sdkproto.ParameterValue, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) {
return r.runTemplateImportProvisionWithRichParameters(ctx, values, variableValues, nil, metadata)
}
// Performs a dry-run provision with provided rich parameters.
// This is used to detect resources that would be provisioned for a workspace in various states.
func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Context, values []*sdkproto.ParameterValue, variableValues []*sdkproto.VariableValue, richParameterValues []*sdkproto.RichParameterValue, metadata *sdkproto.Provision_Metadata) ([]*sdkproto.Resource, []*sdkproto.RichParameter, error) {
func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Context, values []*sdkproto.ParameterValue, variableValues []*sdkproto.VariableValue, richParameterValues []*sdkproto.RichParameterValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
@ -704,7 +711,7 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
// to send the cancel to the provisioner
stream, err := r.provisioner.Provision(ctx)
if err != nil {
return nil, nil, xerrors.Errorf("provision: %w", err)
return nil, xerrors.Errorf("provision: %w", err)
}
defer stream.Close()
go func() {
@ -733,13 +740,13 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
},
})
if err != nil {
return nil, nil, xerrors.Errorf("start provision: %w", err)
return nil, xerrors.Errorf("start provision: %w", err)
}
for {
msg, err := stream.Recv()
if err != nil {
return nil, nil, xerrors.Errorf("recv import provision: %w", err)
return nil, xerrors.Errorf("recv import provision: %w", err)
}
switch msgType := msg.Type.(type) {
case *sdkproto.Provision_Response_Log:
@ -760,11 +767,11 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
slog.F("error", msgType.Complete.Error),
)
return nil, nil, xerrors.New(msgType.Complete.Error)
return nil, xerrors.New(msgType.Complete.Error)
}
if len(msgType.Complete.Parameters) > 0 && len(values) > 0 {
return nil, nil, xerrors.Errorf(`rich parameters can't be used together with legacy parameters, set the coder provider flag "feature_use_managed_variables = true" to enable managed variables`)
return nil, xerrors.Errorf(`rich parameters can't be used together with legacy parameters, set the coder provider flag "feature_use_managed_variables = true" to enable managed variables`)
}
r.logger.Info(context.Background(), "parse dry-run provision successful",
@ -773,9 +780,13 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
slog.F("state_length", len(msgType.Complete.State)),
)
return msgType.Complete.Resources, msgType.Complete.Parameters, nil
return &templateImportProvision{
Resources: msgType.Complete.Resources,
Parameters: msgType.Complete.Parameters,
GitAuthProviders: msgType.Complete.GitAuthProviders,
}, nil
default:
return nil, nil, xerrors.Errorf("invalid message type %q received from provisioner",
return nil, xerrors.Errorf("invalid message type %q received from provisioner",
reflect.TypeOf(msg.Type).String())
}
}
@ -814,7 +825,7 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
}
// Run the template import provision task since it's already a dry run.
resources, _, err := r.runTemplateImportProvisionWithRichParameters(ctx,
provision, err := r.runTemplateImportProvisionWithRichParameters(ctx,
r.job.GetTemplateDryRun().GetParameterValues(),
r.job.GetTemplateDryRun().GetVariableValues(),
r.job.GetTemplateDryRun().GetRichParameterValues(),
@ -828,7 +839,7 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
JobId: r.job.JobId,
Type: &proto.CompletedJob_TemplateDryRun_{
TemplateDryRun: &proto.CompletedJob_TemplateDryRun{
Resources: resources,
Resources: provision.Resources,
},
},
}, nil

File diff suppressed because it is too large Load Diff

View File

@ -118,6 +118,11 @@ message InstanceIdentityAuth {
string instance_id = 1;
}
message GitAuthProvider {
string id = 1;
string access_token = 2;
}
// Agent represents a running agent on the workspace.
message Agent {
string id = 1;
@ -235,8 +240,9 @@ message Provision {
message Plan {
Config config = 1;
repeated ParameterValue parameter_values = 2;
repeated RichParameterValue rich_parameter_values = 3;
repeated VariableValue variable_values = 4;
repeated RichParameterValue rich_parameter_values = 3;
repeated VariableValue variable_values = 4;
repeated GitAuthProvider git_auth_providers = 5;
}
message Apply {
@ -257,7 +263,8 @@ message Provision {
string error = 2;
repeated Resource resources = 3;
repeated RichParameter parameters = 4;
bytes plan = 5;
repeated string git_auth_providers = 5;
bytes plan = 6;
}
message Response {
oneof type {

View File

@ -136,6 +136,6 @@
"chrome 66",
"firefox 63",
"edge 79",
"safari 13.1"
"safari 15.4"
]
}

View File

@ -295,6 +295,15 @@ export const createTemplateVersion = async (
return response.data
}
export const getTemplateVersionGitAuth = async (
versionId: string,
): Promise<TypesGen.TemplateVersionGitAuth[]> => {
const response = await axios.get(
`/api/v2/templateversions/${versionId}/gitauth`,
)
return response.data
}
export const getTemplateVersionParameters = async (
versionId: string,
): Promise<TypesGen.Parameter[]> => {

View File

@ -768,6 +768,14 @@ export interface TemplateVersion {
readonly created_by: User
}
// From codersdk/templateversions.go
export interface TemplateVersionGitAuth {
readonly id: string
readonly type: GitProvider
readonly authenticate_url: string
readonly authenticated: boolean
}
// From codersdk/templateversions.go
export interface TemplateVersionParameter {
readonly name: string
@ -1158,6 +1166,15 @@ export const FeatureNames: FeatureName[] = [
"user_limit",
]
// From codersdk/workspaceagents.go
export type GitProvider = "azure-devops" | "bitbucket" | "github" | "gitlab"
export const GitProviders: GitProvider[] = [
"azure-devops",
"bitbucket",
"github",
"gitlab",
]
// From codersdk/provisionerdaemons.go
export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"
export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"]

View File

@ -0,0 +1,57 @@
import { Story } from "@storybook/react"
import { GitAuth, GitAuthProps } from "./GitAuth"
export default {
title: "components/GitAuth",
component: GitAuth,
}
const Template: Story<GitAuthProps> = (args) => <GitAuth {...args} />
export const GithubNotAuthenticated = Template.bind({})
GithubNotAuthenticated.args = {
type: "github",
authenticated: false,
}
export const GithubAuthenticated = Template.bind({})
GithubAuthenticated.args = {
type: "github",
authenticated: true,
}
export const GitlabNotAuthenticated = Template.bind({})
GitlabNotAuthenticated.args = {
type: "gitlab",
authenticated: false,
}
export const GitlabAuthenticated = Template.bind({})
GitlabAuthenticated.args = {
type: "gitlab",
authenticated: true,
}
export const AzureDevOpsNotAuthenticated = Template.bind({})
AzureDevOpsNotAuthenticated.args = {
type: "azure-devops",
authenticated: false,
}
export const AzureDevOpsAuthenticated = Template.bind({})
AzureDevOpsAuthenticated.args = {
type: "azure-devops",
authenticated: true,
}
export const BitbucketNotAuthenticated = Template.bind({})
BitbucketNotAuthenticated.args = {
type: "bitbucket",
authenticated: false,
}
export const BitbucketAuthenticated = Template.bind({})
BitbucketAuthenticated.args = {
type: "bitbucket",
authenticated: true,
}

View File

@ -0,0 +1,115 @@
import Button from "@material-ui/core/Button"
import FormHelperText from "@material-ui/core/FormHelperText"
import { makeStyles, Theme } from "@material-ui/core/styles"
import { SvgIconProps } from "@material-ui/core/SvgIcon"
import Tooltip from "@material-ui/core/Tooltip"
import GitHub from "@material-ui/icons/GitHub"
import * as TypesGen from "api/typesGenerated"
import { AzureDevOpsIcon } from "components/Icons/AzureDevOpsIcon"
import { BitbucketIcon } from "components/Icons/BitbucketIcon"
import { GitlabIcon } from "components/Icons/GitlabIcon"
import { Typography } from "components/Typography/Typography"
import { FC } from "react"
export interface GitAuthProps {
type: TypesGen.GitProvider
authenticated: boolean
authenticateURL: string
error?: string
}
export const GitAuth: FC<GitAuthProps> = ({
type,
authenticated,
authenticateURL,
error,
}) => {
const styles = useStyles({
error: typeof error !== "undefined",
})
let prettyName: string
let Icon: (props: SvgIconProps) => JSX.Element
switch (type) {
case "azure-devops":
prettyName = "Azure DevOps"
Icon = AzureDevOpsIcon
break
case "bitbucket":
prettyName = "Bitbucket"
Icon = BitbucketIcon
break
case "github":
prettyName = "GitHub"
Icon = GitHub
break
case "gitlab":
prettyName = "GitLab"
Icon = GitlabIcon
break
default:
throw new Error("invalid git provider: " + type)
}
return (
<Tooltip
title={
authenticated ? "You're already authenticated! No action needed." : ``
}
>
<div>
<a
href={authenticateURL}
className={styles.link}
onClick={(event) => {
event.preventDefault()
// If the user is already authenticated, we don't want to redirect them
if (authenticated || authenticateURL === "") {
return
}
window.open(authenticateURL, "_blank", "width=900,height=600")
}}
>
<Button className={styles.button} disabled={authenticated} fullWidth>
<div className={styles.root}>
<Icon className={styles.icon} />
<Typography variant="body2">
{authenticated
? `You're authenticated with ${prettyName}!`
: `Click to login with ${prettyName}!`}
</Typography>
</div>
</Button>
</a>
{error && <FormHelperText error>{error}</FormHelperText>}
</div>
</Tooltip>
)
}
const useStyles = makeStyles<
Theme,
{
error: boolean
}
>((theme) => ({
link: {
textDecoration: "none",
},
root: {
padding: 4,
display: "flex",
gap: 12,
alignItems: "center",
textAlign: "left",
},
button: {
height: "unset",
border: ({ error }) =>
error ? `1px solid ${theme.palette.error.main}` : "unset",
},
icon: {
width: 32,
height: 32,
},
}))

View File

@ -0,0 +1,25 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const AzureDevOpsIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 111 110">
<g clipPath="url(#clip0_1916_993)">
<path
d="M83.0365 94.769L0 82.288L83.0365 110L111 98.365V11.2115L83.0365 0V94.769Z"
fill="#0078D7"
/>
<path
d="M69 27L53.7692 29.7741V63.7029L14 58.5816L30.2885 78V69.8912L53.7692 78L68.7885 65.41"
fill="#0078D7"
/>
<path
d="M53 30C50.0594 27.7015 34.3059 16 34.3059 16V22.4776L13.0913 30.8358L7 38.7761V55.9104L13.5114 58V37.3134L53 30Z"
fill="#0078D7"
/>
</g>
<defs>
<clipPath id="clip0_1916_993">
<rect width="111" height="110" fill="white" />
</clipPath>
</defs>
</SvgIcon>
)

View File

@ -0,0 +1,32 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const BitbucketIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 501 450">
<g clipPath="url(#clip0_1917_1001)">
<path
d="M17.0206 0.0721333C14.6826 0.0419786 12.3663 0.523969 10.2344 1.48427C8.10245 2.44457 6.20658 3.8599 4.67987 5.63088C3.15316 7.40186 2.03262 9.48557 1.39691 11.7357C0.761211 13.9858 0.625758 16.3479 1.00007 18.6559L69.0071 431.504C69.8544 436.556 72.4548 441.148 76.3515 444.474C80.2481 447.799 85.1919 449.645 90.3144 449.688H416.572C420.412 449.737 424.142 448.405 427.082 445.935C430.023 443.465 431.978 440.021 432.592 436.23L500.6 18.736C500.974 16.428 500.838 14.0659 500.203 11.8158C499.567 9.56568 498.446 7.48197 496.92 5.71098C495.393 3.94 493.497 2.52467 491.365 1.56437C489.233 0.604073 486.917 0.122079 484.579 0.152234L17.0206 0.0721333ZM303.387 298.454H199.254L171.058 151.146H328.619L303.387 298.454Z"
fill="#2684FF"
/>
<path
d="M478.972 151.146H328.619L303.387 298.454H199.254L76.2964 444.401C80.1936 447.771 85.1624 449.645 90.3143 449.688H416.652C420.492 449.737 424.222 448.405 427.162 445.935C430.103 443.465 432.058 440.021 432.673 436.23L478.972 151.146Z"
fill="url(#paint0_linear_1917_1001)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_1917_1001"
x1="513.736"
y1="192.398"
x2="265.258"
y2="386.327"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.18" stopColor="#0052CC" />
<stop offset="1" stopColor="#2684FF" />
</linearGradient>
<clipPath id="clip0_1917_1001">
<rect width="501" height="450" fill="white" />
</clipPath>
</defs>
</SvgIcon>
)

View File

@ -0,0 +1,29 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const GitlabIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 194 186">
<g clipPath="url(#clip0_1915_987)">
<path
d="M189.83 73.7299L189.56 73.0399L163.42 4.81995C162.888 3.48288 161.946 2.34863 160.73 1.57996C159.513 0.82434 158.093 0.460411 156.662 0.537307C155.232 0.614203 153.859 1.12822 152.73 2.00996C151.613 2.91701 150.803 4.14609 150.41 5.52995L132.76 59.53H61.2899L43.6399 5.52995C43.2571 4.13855 42.4453 2.90331 41.3199 1.99995C40.1907 1.11822 38.8181 0.6042 37.3875 0.527305C35.9569 0.450409 34.5371 0.814338 33.3199 1.56995C32.1062 2.34173 31.1653 3.47499 30.6299 4.80995L4.43991 73L4.17991 73.69C0.416933 83.522 -0.0475445 94.3109 2.8565 104.43C5.76055 114.549 11.8757 123.45 20.2799 129.79L20.3699 129.86L20.6099 130.03L60.4299 159.85L80.1299 174.76L92.1299 183.82C93.5336 184.886 95.2475 185.463 97.0099 185.463C98.7723 185.463 100.486 184.886 101.89 183.82L113.89 174.76L133.59 159.85L173.65 129.85L173.75 129.77C182.135 123.429 188.236 114.537 191.136 104.432C194.035 94.3262 193.577 83.5527 189.83 73.7299Z"
fill="#E24329"
/>
<path
d="M189.83 73.7299L189.56 73.0399C176.823 75.6543 164.82 81.0495 154.41 88.8399L97 132.25C116.55 147.04 133.57 159.89 133.57 159.89L173.63 129.89L173.73 129.81C182.127 123.469 188.238 114.572 191.141 104.457C194.045 94.3434 193.585 83.5598 189.83 73.7299Z"
fill="#FC6D26"
/>
<path
d="M60.4299 159.89L80.1299 174.8L92.1299 183.86C93.5336 184.926 95.2475 185.503 97.0099 185.503C98.7723 185.503 100.486 184.926 101.89 183.86L113.89 174.8L133.59 159.89C133.59 159.89 116.55 147 96.9999 132.25C77.4499 147 60.4299 159.89 60.4299 159.89Z"
fill="#FCA326"
/>
<path
d="M39.5799 88.84C29.1778 81.0335 17.1779 75.6243 4.43991 73L4.17991 73.69C0.416933 83.5221 -0.0475445 94.311 2.8565 104.43C5.76055 114.549 11.8757 123.45 20.2799 129.79L20.3699 129.86L20.6099 130.03L60.4299 159.85C60.4299 159.85 77.4299 147 96.9999 132.21L39.5799 88.84Z"
fill="#FC6D26"
/>
</g>
<defs>
<clipPath id="clip0_1915_987">
<rect width="194" height="186" fill="white" />
</clipPath>
</defs>
</SvgIcon>
)

View File

@ -12,6 +12,7 @@ import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockTemplateVersionParameter3,
MockTemplateVersionGitAuth,
} from "testHelpers/entities"
import { renderWithAuth } from "testHelpers/renderHelpers"
import CreateWorkspacePage from "./CreateWorkspacePage"
@ -38,6 +39,17 @@ const renderCreateWorkspacePage = () => {
})
}
Object.defineProperty(window, "BroadcastChannel", {
value: class {
addEventListener() {
// noop
}
close() {
// noop
}
},
})
describe("CreateWorkspacePage", () => {
it("renders", async () => {
jest
@ -210,4 +222,24 @@ describe("CreateWorkspacePage", () => {
const validationError = await screen.findByText(validationPatternNotMatched)
expect(validationError).toBeInTheDocument()
})
it("gitauth: errors if unauthenticated and submits", async () => {
jest
.spyOn(API, "getTemplateVersionGitAuth")
.mockResolvedValueOnce([MockTemplateVersionGitAuth])
await waitFor(() => renderCreateWorkspacePage())
const nameField = await screen.findByLabelText(nameLabelText)
// have to use fireEvent b/c userEvent isn't cleaning up properly between tests
fireEvent.change(nameField, {
target: { value: "test" },
})
const submitButton = screen.getByText(createWorkspaceText)
await userEvent.click(submitButton)
await screen.findByText("You must authenticate to create a workspace!")
})
})

View File

@ -33,8 +33,10 @@ const CreateWorkspacePage: FC = () => {
templates,
templateParameters,
templateSchema,
templateGitAuth,
selectedTemplate,
getTemplateSchemaError,
getTemplateGitAuthError,
getTemplatesError,
createWorkspaceError,
permissions,
@ -61,11 +63,14 @@ const CreateWorkspacePage: FC = () => {
selectedTemplate={selectedTemplate}
templateParameters={orderedTemplateParameters(templateParameters)}
templateSchema={templateSchema}
templateGitAuth={templateGitAuth}
createWorkspaceErrors={{
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]:
getTemplateSchemaError,
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError,
[CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR]:
getTemplateGitAuthError,
}}
canCreateForUser={permissions?.createWorkspaceForUser}
owner={owner}

View File

@ -169,3 +169,25 @@ RichParameters.args = {
],
createWorkspaceErrors: {},
}
export const GitAuth = Template.bind({})
GitAuth.args = {
templates: [MockTemplate],
selectedTemplate: MockTemplate,
createWorkspaceErrors: {},
templateParameters: [],
templateGitAuth: [
{
id: "github",
type: "github",
authenticated: false,
authenticate_url: "",
},
{
id: "gitlab",
type: "gitlab",
authenticated: true,
authenticate_url: "",
},
],
}

View File

@ -6,7 +6,7 @@ import { RichParameterInput } from "components/RichParameterInput/RichParameterI
import { Stack } from "components/Stack/Stack"
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
import { FormikContextType, FormikTouched, useFormik } from "formik"
import { FC, useState } from "react"
import { FC, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
import * as Yup from "yup"
@ -15,10 +15,12 @@ import { makeStyles } from "@material-ui/core/styles"
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
import { SelectedTemplate } from "./SelectedTemplate"
import { Loader } from "components/Loader/Loader"
import { GitAuth } from "components/GitAuth/GitAuth"
export enum CreateWorkspaceErrors {
GET_TEMPLATES_ERROR = "getTemplatesError",
GET_TEMPLATE_SCHEMA_ERROR = "getTemplateSchemaError",
GET_TEMPLATE_GITAUTH_ERROR = "getTemplateGitAuthError",
CREATE_WORKSPACE_ERROR = "createWorkspaceError",
}
@ -32,6 +34,7 @@ export interface CreateWorkspacePageViewProps {
selectedTemplate?: TypesGen.Template
templateParameters?: TypesGen.TemplateVersionParameter[]
templateSchema?: TypesGen.ParameterSchema[]
templateGitAuth?: TypesGen.TemplateVersionGitAuth[]
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
canCreateForUser?: boolean
owner: TypesGen.User | null
@ -55,6 +58,15 @@ export const CreateWorkspacePageView: FC<
props.templateParameters,
props.defaultParameterValues,
)
const [gitAuthErrors, setGitAuthErrors] = useState<Record<string, string>>({})
useEffect(() => {
// templateGitAuth is refreshed automatically using a BroadcastChannel
// which may change the `authenticated` property.
//
// If the provider becomes authenticated, we want the error message
// to disappear.
setGitAuthErrors({})
}, [props.templateGitAuth])
const { t } = useTranslation("createWorkspacePage")
@ -75,6 +87,20 @@ export const CreateWorkspacePageView: FC<
enableReinitialize: true,
initialTouched: props.initialTouched,
onSubmit: (request) => {
for (let i = 0; i < (props.templateGitAuth?.length || 0); i++) {
const auth = props.templateGitAuth?.[i]
if (!auth) {
continue
}
if (!auth.authenticated) {
setGitAuthErrors({
[auth.id]: "You must authenticate to create a workspace!",
})
form.setSubmitting(false)
return
}
}
if (!props.templateSchema) {
throw new Error("No template schema loaded")
}
@ -142,6 +168,20 @@ export const CreateWorkspacePageView: FC<
}
/>
)}
{Boolean(
props.createWorkspaceErrors[
CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR
],
) && (
<AlertBanner
severity="error"
error={
props.createWorkspaceErrors[
CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR
]
}
/>
)}
</Stack>
)
}
@ -220,6 +260,37 @@ export const CreateWorkspacePageView: FC<
</div>
)}
{/* Template git auth */}
{props.templateGitAuth && props.templateGitAuth.length > 0 && (
<div className={styles.formSection}>
<div className={styles.formSectionInfo}>
<h2 className={styles.formSectionInfoTitle}>
Git Authentication
</h2>
<p className={styles.formSectionInfoDescription}>
This template requires authentication to automatically perform
Git operations on create.
</p>
</div>
<Stack
direction="column"
spacing={2}
className={styles.formSectionFields}
>
{props.templateGitAuth.map((auth, index) => (
<GitAuth
key={index}
authenticateURL={auth.authenticate_url}
authenticated={auth.authenticated}
type={auth.type}
error={gitAuthErrors[auth.id]}
/>
))}
</Stack>
</div>
)}
{/* Template params */}
{props.templateSchema && props.templateSchema.length > 0 && (
<div className={styles.formSection}>

View File

@ -2,11 +2,21 @@ import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import { SignInLayout } from "components/SignInLayout/SignInLayout"
import { Welcome } from "components/Welcome/Welcome"
import { FC } from "react"
import { FC, useEffect } from "react"
import { Link as RouterLink } from "react-router-dom"
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "xServices/createWorkspace/createWorkspaceXService"
const GitAuthPage: FC = () => {
const styles = useStyles()
useEffect(() => {
// This is used to notify the parent window that the Git auth token has been refreshed.
// It's critical in the create workspace flow!
// eslint-disable-next-line compat/compat -- It actually is supported... not sure why it's complaining.
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL)
// The message doesn't matter, any message refreshes the page!
bc.postMessage("noop")
window.close()
}, [])
return (
<SignInLayout>

View File

@ -1409,3 +1409,10 @@ export const mockParameterSchema = (
...partial,
}
}
export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = {
id: "github",
type: "github",
authenticate_url: "https://example.com/gitauth/github",
authenticated: false,
}

View File

@ -89,6 +89,18 @@ export const handlers = [
)
},
),
rest.get(
"/api/v2/templateversions/:templateVersionId/rich-parameters",
async (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]))
},
),
rest.get(
"/api/v2/templateversions/:templateVersionId/gitauth",
async (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]))
},
),
rest.get(
"api/v2/organizations/:organizationId/templates/:templateName/versions/:templateVersionName",
async (req, res, ctx) => {

View File

@ -2,6 +2,7 @@ import {
checkAuthorization,
createWorkspace,
getTemplates,
getTemplateVersionGitAuth,
getTemplateVersionRichParameters,
getTemplateVersionSchema,
} from "api/api"
@ -9,12 +10,15 @@ import {
CreateWorkspaceRequest,
ParameterSchema,
Template,
TemplateVersionGitAuth,
TemplateVersionParameter,
User,
Workspace,
} from "api/typesGenerated"
import { assign, createMachine } from "xstate"
export const REFRESH_GITAUTH_BROADCAST_CHANNEL = "gitauth_refresh"
type CreateWorkspaceContext = {
organizationId: string
owner: User | null
@ -23,11 +27,13 @@ type CreateWorkspaceContext = {
selectedTemplate?: Template
templateParameters?: TemplateVersionParameter[]
templateSchema?: ParameterSchema[]
templateGitAuth?: TemplateVersionGitAuth[]
createWorkspaceRequest?: CreateWorkspaceRequest
createdWorkspace?: Workspace
createWorkspaceError?: Error | unknown
getTemplatesError?: Error | unknown
getTemplateParametersError?: Error | unknown
getTemplateGitAuthError?: Error | unknown
getTemplateSchemaError?: Error | unknown
permissions?: Record<string, boolean>
checkPermissionsError?: Error | unknown
@ -44,247 +50,305 @@ type SelectOwnerEvent = {
owner: User | null
}
export const createWorkspaceMachine = createMachine(
{
id: "createWorkspaceState",
predictableActionArguments: true,
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
schema: {
context: {} as CreateWorkspaceContext,
events: {} as CreateWorkspaceEvent | SelectOwnerEvent,
services: {} as {
getTemplates: {
data: Template[]
}
getTemplateParameters: {
data: TemplateVersionParameter[]
}
getTemplateSchema: {
data: ParameterSchema[]
}
createWorkspace: {
data: Workspace
}
type RefreshGitAuthEvent = {
type: "REFRESH_GITAUTH"
}
export const createWorkspaceMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QGMBOYCGAXMB1A9qgNawAOGyYAyltmAHQxZYCWAdlACpgC2pANnVgBiCPjYN2AN3xEGaTDgLEyFarRyMwzdl14ChCafmTYW4gNoAGALrWbiUKXywWrcY5AAPRABYATAA0IACeiACMvgCs9ADssQBsVgDM-v5JyQnJUQkAvrnBCnTKJOSUNHRaOhzcfII4ImIS9MZy9EVKhKVqFZpMrDX69XBGbDKm7mz2FuEOSCDOrpOePgjJAJzr9AAcVr4J677bCeFWB3vBYQgBvvQJB-5R6+GxVulPUfmF6MVdquUaBj9XS1AwNYRgVCoQj0MEAM0IPHaP06KjK6kqwMGdUMxgm5imtnsnkWbgJK0QGy2u32h2Op3OvkuiH8vis9HCDxO0XC2z5CX8XxAHTwf3RvSB2gGehxOCoyAAFrwMKJxJIxrJ5CjRWieoCqtLQcN5UqeBhRuMzJYibYSS4yR55qtTv52bFeYlfPEor4XuFmddwlscqz1mdfMlfZlfEKRSV-hi+lKQUM6CblRCoTD4YjkYodd0AZjk9iwdRFcqLSYrYS7Lb5qTlk6Im83R6El7Yj6-QH4gl6M8ctsoht7skrJ8CsLtfHxfqsTKywAFDCoDA8bSQxpqloatpxsV64vVRfDFdrjc4VCwKv4611uZOe1N0DOgXhOKRjZf-axR4Btl2XWWJngSKJtgCHJwnSWMZ0PIskxPI06HPddN2vTNoVQWF6gRVAkQPXUEMlJDUxwVDLy3W8a2mesnyWclmwQTl-A-WIv3WH8Ej-KIAyyW4Em2Z5XV5fx1m48JYPzWcj0Qw0yLAABxNwAEEAFcsAVVVmlaLVpPgxMSPk2UlNUjSFWoyZaMfBZn0Y18WSDfsfUEvlfEHViA2SbZ-HoDZwPSe4omCwUp0IwtDINFMTOUrB1M0zDs1w3NwoTCUotLYZYviiy8Rom0bMbezvEc8T6BcvkII8-1Qj8dZfP2R4fXEziUliKTfiIyKK2QIhdCXSEeBYWBXHEbcdL3eQlV6gb8OG0a2FgYkGzsx0HIQfwfJiKJXmSZJoO88IRwAqwP22aD3OeLshP2DrUQi9Ker6jhZqGkaCRESEsJw7A8II6aiFe+aPuW+iHTYCkNqiKxtgHVi+XWYK2SZWqNr5HZslAiNoJSSdvn0rr0rhFh+H4frV3XEQAGEACUAFEVM4OmAH1cAAeRpgBpKglxUqm6dB2yGLWkq0cecrdv2-xDuO1GXluRH9s5H1wKObi7oLNL9WJ0nyYvEQqDpgAZOmqc4Zm2dwAA5OmacFoqRdWcd0asSWkZuoJUbSftpc2vZ6rZDsXg1mTiLzMwOFDsBtPVGR9zgwn9Q6XQo8sglrLtYWIaYzbxZ2lIpZl5IA3O+hLvWeleSiVj6pDgzHpRFODMS7Cc3w8P7q1ypk8jgy0-ve3Vuz9bc+2yWDvO2WrjE2HMcybZYiOHJYm2fIpzYfAIDgTxUrnOhM-ByGAFoEgDE-6CsS+3n8d0nl9aW8enAmHvnEtTyEA+X1F4cOWryM0k5JyTiAZWS3DSKBdIC9zjZDronY8xkyzpjNJ-YqqxXjORXuEfaEYAh9gAtLO4aQNgenqkdSMsCX7wOisuCmlFrwoMdhETazl9hCQ2NLKwFd1gnRiCkMM51og8k2BQruclqFZTMppBhw9RZBldvQTa0FzgdnuF5Y4ZdgrumyI8JyC8RF700E9fqg1gZjWkZDUBWwDgjjElgnawE1GxAUUJPYglwIjjeDGMKCdKGaB1mTF6tD4ArSzpDfabwdiXxApseqtjT6o1SE4txrVXTpHSF4-GnVfF6QjlAKO5ic5RCOn5LsbwjpHWeJyAMZVThHUgvcCSvp9GyRyTgCABT1rhN8rsV2MTYmgQDC6BR7xuznByCcZpYcvqEA6aLSxdxFa2OyCBWIAYimwxXvwoMrp3GrzXkAA */
createMachine(
{
id: "createWorkspaceState",
predictableActionArguments: true,
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
schema: {
context: {} as CreateWorkspaceContext,
events: {} as
| CreateWorkspaceEvent
| SelectOwnerEvent
| RefreshGitAuthEvent,
services: {} as {
getTemplates: {
data: Template[]
}
getTemplateGitAuth: {
data: TemplateVersionGitAuth[]
}
getTemplateParameters: {
data: TemplateVersionParameter[]
}
getTemplateSchema: {
data: ParameterSchema[]
}
createWorkspace: {
data: Workspace
}
},
},
},
initial: "gettingTemplates",
states: {
gettingTemplates: {
entry: "clearGetTemplatesError",
invoke: {
src: "getTemplates",
onDone: [
{
actions: ["assignTemplates"],
cond: "areTemplatesEmpty",
initial: "gettingTemplates",
states: {
gettingTemplates: {
entry: "clearGetTemplatesError",
invoke: {
src: "getTemplates",
onDone: [
{
actions: ["assignTemplates"],
cond: "areTemplatesEmpty",
},
{
actions: ["assignTemplates", "assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
],
onError: {
actions: ["assignGetTemplatesError"],
target: "error",
},
{
actions: ["assignTemplates", "assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
},
gettingTemplateSchema: {
entry: "clearGetTemplateSchemaError",
invoke: {
src: "getTemplateSchema",
onDone: {
actions: ["assignTemplateSchema"],
target: "gettingTemplateParameters",
},
],
onError: {
actions: ["assignGetTemplatesError"],
target: "error",
},
},
},
gettingTemplateSchema: {
entry: "clearGetTemplateSchemaError",
invoke: {
src: "getTemplateSchema",
onDone: {
actions: ["assignTemplateSchema"],
target: "gettingTemplateParameters",
},
onError: {
actions: ["assignGetTemplateSchemaError"],
target: "error",
},
},
},
gettingTemplateParameters: {
entry: "clearGetTemplateParametersError",
invoke: {
src: "getTemplateParameters",
onDone: {
actions: ["assignTemplateParameters"],
target: "checkingPermissions",
},
onError: {
actions: ["assignGetTemplateParametersError"],
target: "error",
},
},
},
checkingPermissions: {
entry: "clearCheckPermissionsError",
invoke: {
src: "checkPermissions",
id: "checkPermissions",
onDone: {
actions: "assignPermissions",
target: "fillingParams",
},
onError: {
actions: ["assignCheckPermissionsError"],
},
},
},
fillingParams: {
on: {
CREATE_WORKSPACE: {
actions: ["assignCreateWorkspaceRequest", "assignOwner"],
target: "creatingWorkspace",
},
SELECT_OWNER: {
actions: ["assignOwner"],
target: ["fillingParams"],
},
},
},
creatingWorkspace: {
entry: "clearCreateWorkspaceError",
invoke: {
src: "createWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
target: "created",
},
onError: {
actions: ["assignCreateWorkspaceError"],
target: "fillingParams",
},
},
},
created: {
type: "final",
},
error: {},
},
},
{
services: {
getTemplates: (context) => getTemplates(context.organizationId),
getTemplateParameters: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionRichParameters(
selectedTemplate.active_version_id,
)
},
getTemplateSchema: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionSchema(selectedTemplate.active_version_id)
},
checkPermissions: async (context) => {
if (!context.organizationId) {
throw new Error("No organization ID")
}
// HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the
// current user can create a workspace on behalf of anyone within the org (only org owners should be able to do this).
// This pattern should not be replicated outside of this narrow use case.
const permissionsToCheck = {
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: `${context.organizationId}`,
owner_id: "*",
onError: {
actions: ["assignGetTemplateSchemaError"],
target: "error",
},
action: "create",
},
}
return checkAuthorization({
checks: permissionsToCheck,
})
},
createWorkspace: (context) => {
const { createWorkspaceRequest, organizationId, owner } = context
if (!createWorkspaceRequest) {
throw new Error("No create workspace request")
}
return createWorkspace(
organizationId,
owner?.id ?? "me",
createWorkspaceRequest,
)
},
gettingTemplateParameters: {
entry: "clearGetTemplateParametersError",
invoke: {
src: "getTemplateParameters",
onDone: {
actions: ["assignTemplateParameters"],
target: "checkingPermissions",
},
onError: {
actions: ["assignGetTemplateParametersError"],
target: "error",
},
},
},
checkingPermissions: {
entry: "clearCheckPermissionsError",
invoke: {
src: "checkPermissions",
id: "checkPermissions",
onDone: {
actions: "assignPermissions",
target: "gettingTemplateGitAuth",
},
onError: {
actions: ["assignCheckPermissionsError"],
},
},
},
gettingTemplateGitAuth: {
entry: "clearTemplateGitAuthError",
invoke: {
src: "getTemplateGitAuth",
onDone: {
actions: ["assignTemplateGitAuth"],
target: "fillingParams",
},
onError: {
actions: ["assignTemplateGitAuthError"],
target: "error",
},
},
},
fillingParams: {
invoke: {
id: "listenForRefreshGitAuth",
src: () => (callback) => {
// eslint-disable-next-line compat/compat -- It actually is supported... not sure why eslint is complaining.
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL)
bc.addEventListener("message", () => {
callback("REFRESH_GITAUTH")
})
return () => bc.close()
},
},
on: {
CREATE_WORKSPACE: {
actions: ["assignCreateWorkspaceRequest", "assignOwner"],
target: "creatingWorkspace",
},
SELECT_OWNER: {
actions: ["assignOwner"],
target: ["fillingParams"],
},
REFRESH_GITAUTH: {
target: "gettingTemplateGitAuth",
},
},
},
creatingWorkspace: {
entry: "clearCreateWorkspaceError",
invoke: {
src: "createWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
target: "created",
},
onError: {
actions: ["assignCreateWorkspaceError"],
target: "fillingParams",
},
},
},
created: {
type: "final",
},
error: {},
},
},
guards: {
areTemplatesEmpty: (_, event) => event.data.length === 0,
},
actions: {
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignSelectedTemplate: assign({
selectedTemplate: (ctx, event) => {
const templates = event.data.filter(
(template) => template.name === ctx.templateName,
{
services: {
getTemplates: (context) => getTemplates(context.organizationId),
getTemplateGitAuth: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionGitAuth(selectedTemplate.active_version_id)
},
getTemplateParameters: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionRichParameters(
selectedTemplate.active_version_id,
)
return templates.length > 0 ? templates[0] : undefined
},
}),
assignTemplateParameters: assign({
templateParameters: (_, event) => event.data,
}),
assignTemplateSchema: assign({
// Only show parameters that are allowed to be overridden.
// CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155
templateSchema: (_, event) => event.data,
}),
assignPermissions: assign({
permissions: (_, event) => event.data as Record<string, boolean>,
}),
assignCheckPermissionsError: assign({
checkPermissionsError: (_, event) => event.data,
}),
clearCheckPermissionsError: assign({
checkPermissionsError: (_) => undefined,
}),
assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request,
}),
assignOwner: assign({
owner: (_, event) => event.owner,
}),
assignCreateWorkspaceError: assign({
createWorkspaceError: (_, event) => event.data,
}),
clearCreateWorkspaceError: assign({
createWorkspaceError: (_) => undefined,
}),
assignGetTemplatesError: assign({
getTemplatesError: (_, event) => event.data,
}),
clearGetTemplatesError: assign({
getTemplatesError: (_) => undefined,
}),
assignGetTemplateParametersError: assign({
getTemplateParametersError: (_, event) => event.data,
}),
clearGetTemplateParametersError: assign({
getTemplateParametersError: (_) => undefined,
}),
assignGetTemplateSchemaError: assign({
getTemplateSchemaError: (_, event) => event.data,
}),
clearGetTemplateSchemaError: assign({
getTemplateSchemaError: (_) => undefined,
}),
getTemplateSchema: (context) => {
const { selectedTemplate } = context
if (!selectedTemplate) {
throw new Error("No selected template")
}
return getTemplateVersionSchema(selectedTemplate.active_version_id)
},
checkPermissions: async (context) => {
if (!context.organizationId) {
throw new Error("No organization ID")
}
// HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the
// current user can create a workspace on behalf of anyone within the org (only org owners should be able to do this).
// This pattern should not be replicated outside of this narrow use case.
const permissionsToCheck = {
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: `${context.organizationId}`,
owner_id: "*",
},
action: "create",
},
}
return checkAuthorization({
checks: permissionsToCheck,
})
},
createWorkspace: (context) => {
const { createWorkspaceRequest, organizationId, owner } = context
if (!createWorkspaceRequest) {
throw new Error("No create workspace request")
}
return createWorkspace(
organizationId,
owner?.id ?? "me",
createWorkspaceRequest,
)
},
},
guards: {
areTemplatesEmpty: (_, event) => event.data.length === 0,
},
actions: {
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignSelectedTemplate: assign({
selectedTemplate: (ctx, event) => {
const templates = event.data.filter(
(template) => template.name === ctx.templateName,
)
return templates.length > 0 ? templates[0] : undefined
},
}),
assignTemplateParameters: assign({
templateParameters: (_, event) => event.data,
}),
assignTemplateSchema: assign({
// Only show parameters that are allowed to be overridden.
// CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155
templateSchema: (_, event) => event.data,
}),
assignPermissions: assign({
permissions: (_, event) => event.data as Record<string, boolean>,
}),
assignCheckPermissionsError: assign({
checkPermissionsError: (_, event) => event.data,
}),
clearCheckPermissionsError: assign({
checkPermissionsError: (_) => undefined,
}),
assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request,
}),
assignOwner: assign({
owner: (_, event) => event.owner,
}),
assignCreateWorkspaceError: assign({
createWorkspaceError: (_, event) => event.data,
}),
clearCreateWorkspaceError: assign({
createWorkspaceError: (_) => undefined,
}),
assignGetTemplatesError: assign({
getTemplatesError: (_, event) => event.data,
}),
clearGetTemplatesError: assign({
getTemplatesError: (_) => undefined,
}),
assignGetTemplateParametersError: assign({
getTemplateParametersError: (_, event) => event.data,
}),
clearGetTemplateParametersError: assign({
getTemplateParametersError: (_) => undefined,
}),
assignGetTemplateSchemaError: assign({
getTemplateSchemaError: (_, event) => event.data,
}),
clearGetTemplateSchemaError: assign({
getTemplateSchemaError: (_) => undefined,
}),
clearTemplateGitAuthError: assign({
getTemplateGitAuthError: (_) => undefined,
}),
assignTemplateGitAuthError: assign({
getTemplateGitAuthError: (_, event) => event.data,
}),
assignTemplateGitAuth: assign({
templateGitAuth: (_, event) => event.data,
}),
},
},
},
)
)