Ctrl K
ComunidadGuías…

coderd/provisionerjobs_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd_test

import (
	"context"
	"database/sql"
	"encoding/json"
	"strconv"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/provisioner/echo"
	"github.com/coder/coder/v2/provisionersdk/proto"
	"github.com/coder/coder/v2/testutil"
)

func TestProvisionerJobs(t *testing.T) {
	t.Parallel()

	t.Run("ProvisionerJobs", func(t *testing.T) {
		db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t, dbtestutil.WithDumpOnFailure())
		client := coderdtest.New(t, &coderdtest.Options{
			IncludeProvisionerDaemon: true,
			Database:                 db,
			Pubsub:                   ps,
		})
		owner := coderdtest.CreateFirstUser(t, client)
		templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
		memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		workspace := coderdtest.CreateWorkspace(t, client, template.ID)
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

		// Ensure the workspace build job has a different timestamp from
		// the template version job for sorting, without sleeping.
		_, err := sqlDB.ExecContext(context.Background(),
			"UPDATE provisioner_jobs SET created_at = created_at + INTERVAL '2 seconds' WHERE id = $1",
			workspace.LatestBuild.Job.ID,
		)
		require.NoError(t, err)

		// Create a pending job.
		w := dbgen.Workspace(t, db, database.WorkspaceTable{
			OrganizationID: owner.OrganizationID,
			OwnerID:        member.ID,
			TemplateID:     template.ID,
		})
		wbID := uuid.New()
		job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
			OrganizationID: w.OrganizationID,
			StartedAt:      sql.NullTime{Time: dbtime.Now(), Valid: true},
			Type:           database.ProvisionerJobTypeWorkspaceBuild,
			Input:          json.RawMessage(`{"workspace_build_id":"` + wbID.String() + `"}`),
			InitiatorID:    member.ID,
			Tags:           database.StringMap{"initiatorTest": "true"},
		})
		dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
			ID:                wbID,
			JobID:             job.ID,
			WorkspaceID:       w.ID,
			TemplateVersionID: version.ID,
		})

		// Add more jobs than the default limit.
		for i := range 60 {
			dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
				OrganizationID: owner.OrganizationID,
				Tags:           database.StringMap{"count": strconv.Itoa(i)},
				InitiatorID:    owner.UserID,
			})
		}

		t.Run("Single", func(t *testing.T) {
			t.Parallel()
			t.Run("Workspace", func(t *testing.T) {
				t.Parallel()
				t.Run("OK", func(t *testing.T) {
					t.Parallel()
					ctx := testutil.Context(t, testutil.WaitMedium)
					// Note this calls the single job endpoint.
					job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID)
					require.NoError(t, err)
					require.Equal(t, job.ID, job2.ID)

					// Verify that job metadata is correct.
					assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{
						TemplateVersionName:      version.Name,
						TemplateID:               template.ID,
						TemplateName:             template.Name,
						TemplateDisplayName:      template.DisplayName,
						TemplateIcon:             template.Icon,
						WorkspaceID:              &w.ID,
						WorkspaceName:            w.Name,
						WorkspaceBuildTransition: codersdk.WorkspaceTransitionStart,
					})
				})
			})
			t.Run("Template Import", func(t *testing.T) {
				t.Parallel()
				t.Run("OK", func(t *testing.T) {
					t.Parallel()
					ctx := testutil.Context(t, testutil.WaitMedium)
					// Note this calls the single job endpoint.
					job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, version.Job.ID)
					require.NoError(t, err)
					require.Equal(t, version.Job.ID, job2.ID)

					// Verify that job metadata is correct.
					assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{
						TemplateVersionName: version.Name,
						TemplateID:          template.ID,
						TemplateName:        template.Name,
						TemplateDisplayName: template.DisplayName,
						TemplateIcon:        template.Icon,
					})
				})
			})
			t.Run("Missing", func(t *testing.T) {
				t.Parallel()
				ctx := testutil.Context(t, testutil.WaitMedium)
				// Note this calls the single job endpoint.
				_, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, uuid.New())
				require.Error(t, err)
			})
		})

		t.Run("Default limit", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil)
			require.NoError(t, err)
			require.Len(t, jobs, 50)
		})

		t.Run("IDs", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				IDs: []uuid.UUID{workspace.LatestBuild.Job.ID, version.Job.ID},
			})
			require.NoError(t, err)
			require.Len(t, jobs, 2)
		})

		t.Run("Status", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Status: []codersdk.ProvisionerJobStatus{codersdk.ProvisionerJobRunning},
			})
			require.NoError(t, err)
			require.Len(t, jobs, 1)
		})

		t.Run("Tags", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Tags: map[string]string{"count": "1"},
			})
			require.NoError(t, err)
			require.Len(t, jobs, 1)
		})

		t.Run("Initiator", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: member.ID.String(),
			})
			require.NoError(t, err)
			require.GreaterOrEqual(t, len(jobs), 1)
			require.Equal(t, member.ID, jobs[0].InitiatorID)
		})

		t.Run("InitiatorWithOtherFilters", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			// Test filtering by initiator ID combined with status filter
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: owner.UserID.String(),
				Status:    []codersdk.ProvisionerJobStatus{codersdk.ProvisionerJobSucceeded},
			})
			require.NoError(t, err)

			// Verify all returned jobs have the correct initiator and status
			for _, job := range jobs {
				require.Equal(t, owner.UserID, job.InitiatorID)
				require.Equal(t, codersdk.ProvisionerJobSucceeded, job.Status)
			}
		})

		t.Run("InitiatorWithLimit", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			// Test filtering by initiator ID with limit
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: owner.UserID.String(),
				Limit:     1,
			})
			require.NoError(t, err)
			require.Len(t, jobs, 1)

			// Verify the returned job has the correct initiator
			require.Equal(t, owner.UserID, jobs[0].InitiatorID)
		})

		t.Run("InitiatorWithTags", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			// Test filtering by initiator ID combined with tags
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: member.ID.String(),
				Tags:      map[string]string{"initiatorTest": "true"},
			})
			require.NoError(t, err)
			require.Len(t, jobs, 1)

			// Verify the returned job has the correct initiator and tags
			require.Equal(t, member.ID, jobs[0].InitiatorID)
			require.Equal(t, "true", jobs[0].Tags["initiatorTest"])
		})

		t.Run("InitiatorNotFound", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			// Test with non-existent initiator ID
			nonExistentID := uuid.New()
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: nonExistentID.String(),
			})
			require.NoError(t, err)
			require.Len(t, jobs, 0)
		})

		t.Run("InitiatorNil", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)

			// Test with nil initiator ID (should return all jobs)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: "",
			})
			require.NoError(t, err)
			require.GreaterOrEqual(t, len(jobs), 50) // Should return all jobs (up to default limit)
		})

		t.Run("Limit", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Limit: 1,
			})
			require.NoError(t, err)
			require.Len(t, jobs, 1)
		})

		// For now, this is not allowed even though the member has created a
		// workspace. Once member-level permissions for jobs are supported
		// by RBAC, this test should be updated.
		t.Run("MemberDenied", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			jobs, err := memberClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil)
			require.Error(t, err)
			require.Len(t, jobs, 0)
		})

		t.Run("MemberDeniedWithInitiator", func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitMedium)
			// Member should not be able to access jobs even with initiator filter
			jobs, err := memberClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{
				Initiator: member.ID.String(),
			})
			require.Error(t, err)
			require.Len(t, jobs, 0)
		})
	})

	// Ensures that when a provisioner job is in the succeeded state,
	// the API response includes both worker_id and worker_name fields
	t.Run("AssignedProvisionerJob", func(t *testing.T) {
		t.Parallel()

		db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
		client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
			IncludeProvisionerDaemon: false,
			Database:                 db,
			Pubsub:                   ps,
		})
		provisionerDaemonName := "provisioner_daemon_test"
		provisionerDaemon := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, provisionerDaemonName, map[string]string{"owner": "", "scope": "organization"})
		owner := coderdtest.CreateFirstUser(t, client)
		templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))

		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		workspace := coderdtest.CreateWorkspace(t, client, template.ID)
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

		// Stop the provisioner so it doesn't grab any more jobs
		err := provisionerDaemon.Close()
		require.NoError(t, err)

		t.Run("List_IncludesWorkerIDAndName", func(t *testing.T) {
			t.Parallel()

			ctx := testutil.Context(t, testutil.WaitMedium)

			// Get provisioner daemon responsible for executing the provisioner jobs
			provisionerDaemons, err := db.GetProvisionerDaemons(ctx)
			require.NoError(t, err)
			require.Equal(t, 1, len(provisionerDaemons))
			if assert.NotEmpty(t, provisionerDaemons) {
				require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name)
			}

			// Get provisioner jobs
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil)
			require.NoError(t, err)
			require.Equal(t, 2, len(jobs))

			for _, job := range jobs {
				require.Equal(t, owner.OrganizationID, job.OrganizationID)
				require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(job.Status))

				// Guarantee that provisioner jobs contain the provisioner daemon ID and name
				if assert.NotEmpty(t, provisionerDaemons) {
					require.Equal(t, &provisionerDaemons[0].ID, job.WorkerID)
					require.Equal(t, provisionerDaemonName, job.WorkerName)
				}
			}
		})

		t.Run("Get_IncludesWorkerIDAndName", func(t *testing.T) {
			t.Parallel()

			ctx := testutil.Context(t, testutil.WaitMedium)

			// Get provisioner daemon responsible for executing the provisioner job
			provisionerDaemons, err := db.GetProvisionerDaemons(ctx)
			require.NoError(t, err)
			require.Equal(t, 1, len(provisionerDaemons))
			if assert.NotEmpty(t, provisionerDaemons) {
				require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name)
			}

			// Get all provisioner jobs
			jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil)
			require.NoError(t, err)
			require.Equal(t, 2, len(jobs))

			// Find workspace_build provisioner job ID
			var workspaceProvisionerJobID uuid.UUID
			for _, job := range jobs {
				if job.Type == codersdk.ProvisionerJobTypeWorkspaceBuild {
					workspaceProvisionerJobID = job.ID
				}
			}
			require.NotNil(t, workspaceProvisionerJobID)

			// Get workspace_build provisioner job by ID
			workspaceProvisionerJob, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, workspaceProvisionerJobID)
			require.NoError(t, err)

			require.Equal(t, owner.OrganizationID, workspaceProvisionerJob.OrganizationID)
			require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(workspaceProvisionerJob.Status))

			// Guarantee that provisioner job contains the provisioner daemon ID and name
			if assert.NotEmpty(t, provisionerDaemons) {
				require.Equal(t, &provisionerDaemons[0].ID, workspaceProvisionerJob.WorkerID)
				require.Equal(t, provisionerDaemonName, workspaceProvisionerJob.WorkerName)
			}
		})
	})
}

func TestProvisionerJobLogs(t *testing.T) {
	t.Parallel()
	t.Run("StreamAfterComplete", 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, &echo.Responses{
			Parse: echo.ParseComplete,
			ProvisionApply: []*proto.Response{{
				Type: &proto.Response_Log{
					Log: &proto.Log{
						Level:  proto.LogLevel_INFO,
						Output: "log-output",
					},
				},
			}, {
				Type: &proto.Response_Apply{
					Apply: &proto.ApplyComplete{},
				},
			}},
		})
		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		workspace := coderdtest.CreateWorkspace(t, client, template.ID)
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, 0)
		require.NoError(t, err)
		defer closer.Close()
		for {
			log, ok := <-logs
			t.Logf("got log: [%s] %s %s", log.Level, log.Stage, log.Output)
			if !ok {
				return
			}
		}
	})

	t.Run("StreamWhileRunning", 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, &echo.Responses{
			Parse: echo.ParseComplete,
			ProvisionApply: []*proto.Response{{
				Type: &proto.Response_Log{
					Log: &proto.Log{
						Level:  proto.LogLevel_INFO,
						Output: "log-output",
					},
				},
			}, {
				Type: &proto.Response_Apply{
					Apply: &proto.ApplyComplete{},
				},
			}},
		})
		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		workspace := coderdtest.CreateWorkspace(t, client, template.ID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, 0)
		require.NoError(t, err)
		defer closer.Close()
		for {
			_, ok := <-logs
			if !ok {
				return
			}
		}
	})
}