package coderd_test import ( "context" "fmt" "net/http" "strconv" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" ) func TestWorkspaceBuild(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) } func TestWorkspaceBuildByBuildNumber(t *testing.T) { t.Parallel() t.Run("Successful", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( context.Background(), user.Username, workspace.Name, strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), ) require.NoError(t, err) }) t.Run("BuildNumberNotInt", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( context.Background(), user.Username, workspace.Name, "buildNumber", ) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusBadRequest, apiError.StatusCode()) require.ErrorContains(t, apiError, "Failed to parse build number as integer.") }) t.Run("WorkspaceNotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( context.Background(), user.Username, "workspaceName", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), ) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusNotFound, apiError.StatusCode()) require.ErrorContains(t, apiError, "Resource not found") }) t.Run("WorkspaceBuildNotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( context.Background(), user.Username, workspace.Name, "200", ) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusNotFound, apiError.StatusCode()) require.ErrorContains(t, apiError, fmt.Sprintf("Workspace %q Build 200 does not exist.", workspace.Name)) }) } func TestWorkspaceBuilds(t *testing.T) { t.Parallel() t.Run("Single", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) builds, err := client.WorkspaceBuilds(context.Background(), codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID}) require.Len(t, builds, 1) require.Equal(t, int32(1), builds[0].BuildNumber) require.Equal(t, user.Username, builds[0].InitiatorUsername) require.NoError(t, err) }) t.Run("PaginateNonExistentRow", func(t *testing.T) { t.Parallel() ctx := context.Background() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) _, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{ WorkspaceID: workspace.ID, Pagination: codersdk.Pagination{ AfterID: uuid.New(), }, }) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusBadRequest, apiError.StatusCode()) require.Contains(t, apiError.Message, "does not exist") }) t.Run("PaginateLimitOffset", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) var expectedBuilds []codersdk.WorkspaceBuild extraBuilds := 4 for i := 0; i < extraBuilds; i++ { b := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart) expectedBuilds = append(expectedBuilds, b) coderdtest.AwaitWorkspaceBuildJob(t, client, b.ID) } pageSize := 3 firstPage, err := client.WorkspaceBuilds(context.Background(), codersdk.WorkspaceBuildsRequest{ WorkspaceID: workspace.ID, Pagination: codersdk.Pagination{Limit: pageSize, Offset: 0}, }) require.NoError(t, err) require.Len(t, firstPage, pageSize) for i := 0; i < pageSize; i++ { require.Equal(t, expectedBuilds[extraBuilds-i-1].ID, firstPage[i].ID) } secondPage, err := client.WorkspaceBuilds(context.Background(), codersdk.WorkspaceBuildsRequest{ WorkspaceID: workspace.ID, Pagination: codersdk.Pagination{Limit: pageSize, Offset: pageSize}, }) require.NoError(t, err) require.Len(t, secondPage, 2) require.Equal(t, expectedBuilds[0].ID, secondPage[0].ID) require.Equal(t, workspace.LatestBuild.ID, secondPage[1].ID) // build created while creating workspace }) } func TestPatchCancelWorkspaceBuild(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ Log: &proto.Log{}, }, }}, ProvisionDryRun: echo.ProvisionComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) var build codersdk.WorkspaceBuild require.Eventually(t, func() bool { var err error build, err = client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID) return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning }, testutil.WaitShort, testutil.IntervalFast) err := client.CancelWorkspaceBuild(context.Background(), build.ID) require.NoError(t, err) require.Eventually(t, func() bool { var err error build, err = client.WorkspaceBuild(context.Background(), build.ID) return assert.NoError(t, err) && // The job will never actually cancel successfully because it will never send a // provision complete response. assert.Empty(t, build.Job.Error) && build.Job.Status == codersdk.ProvisionerJobCanceling }, testutil.WaitShort, testutil.IntervalFast) } func TestWorkspaceBuildResources(t *testing.T) { t.Parallel() t.Run("ListRunning", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerD: true, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) }) t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, }}, }, { Name: "another", Type: "example", }}, }, }, }}, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) require.NotNil(t, resources) require.Len(t, resources, 2) require.Equal(t, "some", resources[0].Name) require.Equal(t, "example", resources[0].Type) require.Len(t, resources[0].Agents, 1) }) } func TestWorkspaceBuildLogs(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) before := time.Now() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "example", }, }, }, { Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, }}, }, { Name: "another", Type: "example", }}, }, }, }}, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() logs, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, before.Add(-time.Hour)) require.NoError(t, err) for { log, ok := <-logs if !ok { break } if log.Output == "example" { return } } require.Fail(t, "example message never happened") } func TestWorkspaceBuildState(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) wantState := []byte("some kinda state") version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionDryRun: echo.ProvisionComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ State: wantState, }, }, }}, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) gotState, err := client.WorkspaceBuildState(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) require.Equal(t, wantState, gotState) }