mirror of https://github.com/coder/coder.git
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:
parent
3d8b77d6f1
commit
7f226d4f90
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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{})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE template_versions
|
||||
DROP COLUMN git_auth_providers;
|
|
@ -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';
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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 *;
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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(¶ms)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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> |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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, ¶m)
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
24
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.dot
generated
vendored
Normal file
24
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.dot
generated
vendored
Normal 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)"
|
||||
}
|
||||
}
|
||||
|
212
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json
generated
vendored
Normal file
212
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json
generated
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
24
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.dot
generated
vendored
Normal file
24
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.dot
generated
vendored
Normal 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)"
|
||||
}
|
||||
}
|
||||
|
79
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json
generated
vendored
Normal file
79
provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json
generated
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
@ -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 {
|
||||
|
|
|
@ -136,6 +136,6 @@
|
|||
"chrome 66",
|
||||
"firefox 63",
|
||||
"edge 79",
|
||||
"safari 13.1"
|
||||
"safari 15.4"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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[]> => {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}))
|
|
@ -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>
|
||||
)
|
|
@ -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>
|
||||
)
|
|
@ -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>
|
||||
)
|
|
@ -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!")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue