From 7887a70e21b1d2005f25a6d645524b41c3eadfea Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 03:14:08 +0000 Subject: [PATCH 1/8] chore: remove deprecated agent v1 routes --- coderd/coderd.go | 12 - coderd/deprecated.go | 56 --- coderd/workspaceagents.go | 645 --------------------------------- coderd/workspaceagents_test.go | 100 ----- coderd/workspaceagentsrpc.go | 25 -- coderd/workspacebuilds_test.go | 2 +- 6 files changed, 1 insertion(+), 839 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 60bfe9813c559..bbcc080326ba5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1004,23 +1004,11 @@ func New(options *Options) *API { Optional: false, })) r.Get("/rpc", api.workspaceAgentRPC) - r.Get("/manifest", api.workspaceAgentManifest) - // This route is deprecated and will be removed in a future release. - // New agents will use /me/manifest instead. - r.Get("/metadata", api.workspaceAgentManifest) - r.Post("/startup", api.postWorkspaceAgentStartup) - r.Patch("/startup-logs", api.patchWorkspaceAgentLogsDeprecated) r.Patch("/logs", api.patchWorkspaceAgentLogs) - r.Post("/app-health", api.postWorkspaceAppHealth) // Deprecated: Required to support legacy agents r.Get("/gitauth", api.workspaceAgentsGitAuth) r.Get("/external-auth", api.workspaceAgentsExternalAuth) r.Get("/gitsshkey", api.agentGitSSHKey) - r.Get("/coordinate", api.workspaceAgentCoordinate) - r.Post("/report-stats", api.workspaceAgentReportStats) - r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle) - r.Post("/metadata", api.workspaceAgentPostMetadata) - r.Post("/metadata/{key}", api.workspaceAgentPostMetadataDeprecated) r.Post("/log-source", api.workspaceAgentPostLogSource) }) r.Route("/{workspaceagent}", func(r chi.Router) { diff --git a/coderd/deprecated.go b/coderd/deprecated.go index 762b5bc931e38..6dc03e540ce33 100644 --- a/coderd/deprecated.go +++ b/coderd/deprecated.go @@ -3,13 +3,9 @@ package coderd import ( "net/http" - "github.com/go-chi/chi/v5" - - "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" ) // @Summary Removed: Get parameters by template version @@ -34,19 +30,6 @@ func templateVersionSchemaDeprecated(rw http.ResponseWriter, r *http.Request) { httpapi.Write(r.Context(), rw, http.StatusOK, []struct{}{}) } -// @Summary Removed: Patch workspace agent logs -// @ID removed-patch-workspace-agent-logs -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Agents -// @Param request body agentsdk.PatchLogs true "logs" -// @Success 200 {object} codersdk.Response -// @Router /workspaceagents/me/startup-logs [patch] -func (api *API) patchWorkspaceAgentLogsDeprecated(rw http.ResponseWriter, r *http.Request) { - api.patchWorkspaceAgentLogs(rw, r) -} - // @Summary Removed: Get logs by workspace agent // @ID removed-get-logs-by-workspace-agent // @Security CoderSessionToken @@ -77,45 +60,6 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) api.workspaceAgentsExternalAuth(rw, r) } -// @Summary Removed: Submit workspace agent metadata -// @ID removed-submit-workspace-agent-metadata -// @Security CoderSessionToken -// @Accept json -// @Tags Agents -// @Param request body agentsdk.PostMetadataRequestDeprecated true "Workspace agent metadata request" -// @Param key path string true "metadata key" format(string) -// @Success 204 "Success" -// @Router /workspaceagents/me/metadata/{key} [post] -// @x-apidocgen {"skip": true} -func (api *API) workspaceAgentPostMetadataDeprecated(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var req agentsdk.PostMetadataRequestDeprecated - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - workspaceAgent := httpmw.WorkspaceAgent(r) - - key := chi.URLParam(r, "key") - - err := api.workspaceAgentUpdateMetadata(ctx, workspaceAgent, agentsdk.PostMetadataRequest{ - Metadata: []agentsdk.Metadata{ - { - Key: key, - WorkspaceAgentMetadataResult: req, - }, - }, - }) - if err != nil { - api.Logger.Error(ctx, "failed to handle metadata request", slog.Error(err)) - httpapi.InternalServerError(rw, err) - return - } - - httpapi.Write(ctx, rw, http.StatusNoContent, nil) -} - // @Summary Removed: Get workspace resources for workspace build // @ID removed-get-workspace-resources-for-workspace-build // @Security CoderSessionToken diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index c45fae8726480..e9e2ab18027d9 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -18,14 +18,12 @@ import ( "github.com/sqlc-dev/pqtype" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "nhooyr.io/websocket" "tailscale.com/tailcfg" "cdr.dev/slog" - agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" @@ -136,144 +134,8 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, apiAgent) } -// @Summary Get authorized workspace agent manifest -// @ID get-authorized-workspace-agent-manifest -// @Security CoderSessionToken -// @Produce json -// @Tags Agents -// @Success 200 {object} agentsdk.Manifest -// @Router /workspaceagents/me/manifest [get] -func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgent(r) - - // As this API becomes deprecated, use the new protobuf API and convert the - // types back to the SDK types. - manifestAPI := &agentapi.ManifestAPI{ - AccessURL: api.AccessURL, - AppHostname: api.AppHostname, - ExternalAuthConfigs: api.ExternalAuthConfigs, - DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(), - DerpForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), - - AgentFn: func(_ context.Context) (database.WorkspaceAgent, error) { return workspaceAgent, nil }, - WorkspaceIDFn: func(ctx context.Context, wa *database.WorkspaceAgent) (uuid.UUID, error) { - // Sadly this results in a double query, but it's only temporary for - // now. - ws, err := api.Database.GetWorkspaceByAgentID(ctx, wa.ID) - if err != nil { - return uuid.Nil, err - } - return ws.Workspace.ID, nil - }, - Database: api.Database, - DerpMapFn: api.DERPMap, - } - manifest, err := manifestAPI.GetManifest(ctx, &agentproto.GetManifestRequest{}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agent manifest.", - Detail: err.Error(), - }) - return - } - sdkManifest, err := agentsdk.ManifestFromProto(manifest) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error converting manifest.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, sdkManifest) -} - const AgentAPIVersionREST = "1.0" -// @Summary Submit workspace agent startup -// @ID submit-workspace-agent-startup -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Agents -// @Param request body agentsdk.PostStartupRequest true "Startup request" -// @Success 200 -// @Router /workspaceagents/me/startup [post] -// @x-apidocgen {"skip": true} -func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgent(r) - apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout, - api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), - ) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error reading workspace agent.", - Detail: err.Error(), - }) - return - } - - var req agentsdk.PostStartupRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - api.Logger.Debug( - ctx, - "post workspace agent version", - slog.F("agent_id", apiAgent.ID), - slog.F("agent_version", req.Version), - slog.F("remote_addr", r.RemoteAddr), - ) - - if !semver.IsValid(req.Version) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid workspace agent version provided.", - Detail: fmt.Sprintf("invalid semver version: %q", req.Version), - }) - return - } - - // Validate subsystems. - seen := make(map[codersdk.AgentSubsystem]bool) - for _, s := range req.Subsystems { - if !s.Valid() { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid workspace agent subsystem provided.", - Detail: fmt.Sprintf("invalid subsystem: %q", s), - }) - return - } - if seen[s] { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid workspace agent subsystem provided.", - Detail: fmt.Sprintf("duplicate subsystem: %q", s), - }) - return - } - seen[s] = true - } - - if err := api.Database.UpdateWorkspaceAgentStartupByID(ctx, database.UpdateWorkspaceAgentStartupByIDParams{ - ID: apiAgent.ID, - Version: req.Version, - ExpandedDirectory: req.ExpandedDirectory, - Subsystems: convertWorkspaceAgentSubsystems(req.Subsystems), - APIVersion: AgentAPIVersionREST, - }); err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Error setting agent version", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, nil) -} - // @Summary Patch workspace agent logs // @ID patch-workspace-agent-logs // @Security CoderSessionToken @@ -938,79 +800,6 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { } } -// @Summary Coordinate workspace agent via Tailnet -// @Description It accepts a WebSocket connection to an agent that listens to -// @Description incoming connections and publishes node updates. -// @ID coordinate-workspace-agent-via-tailnet -// @Security CoderSessionToken -// @Tags Agents -// @Success 101 -// @Router /workspaceagents/me/coordinate [get] -func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - api.WebsocketWaitMutex.Lock() - api.WebsocketWaitGroup.Add(1) - api.WebsocketWaitMutex.Unlock() - defer api.WebsocketWaitGroup.Done() - // The middleware only accept agents for resources on the latest build. - workspaceAgent := httpmw.WorkspaceAgent(r) - build := httpmw.LatestBuild(r) - - workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), - }) - return - } - - owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Internal error fetching user.", - Detail: err.Error(), - }) - return - } - - conn, err := websocket.Accept(rw, r, nil) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to accept websocket.", - Detail: err.Error(), - }) - return - } - - ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageBinary) - defer wsNetConn.Close() - - closeCtx, closeCtxCancel := context.WithCancel(ctx) - defer closeCtxCancel() - monitor := api.startAgentWebsocketMonitor(closeCtx, workspaceAgent, build, conn) - defer monitor.close() - - api.Logger.Debug(ctx, "accepting agent", - slog.F("owner", owner.Username), - slog.F("workspace", workspace.Name), - slog.F("name", workspaceAgent.Name), - ) - api.Logger.Debug(ctx, "accepting agent details", slog.F("agent", workspaceAgent)) - - defer conn.Close(websocket.StatusNormalClosure, "") - - err = (*api.TailnetCoordinator.Load()).ServeAgent(wsNetConn, workspaceAgent.ID, - fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), - ) - if err != nil { - api.Logger.Warn(ctx, "tailnet coordinator agent error", slog.Error(err)) - _ = conn.Close(websocket.StatusInternalError, err.Error()) - return - } -} - // workspaceAgentClientCoordinate accepts a WebSocket that reads node network updates. // After accept a PubSub starts listening for new connection node updates // which are written to the WebSocket. @@ -1171,214 +960,6 @@ func convertScripts(dbScripts []database.WorkspaceAgentScript) []codersdk.Worksp return scripts } -// @Summary Submit workspace agent stats -// @ID submit-workspace-agent-stats -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Agents -// @Param request body agentsdk.Stats true "Stats request" -// @Success 200 {object} agentsdk.StatsResponse -// @Router /workspaceagents/me/report-stats [post] -// @Deprecated Uses agent API v2 endpoint instead. -func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - workspaceAgent := httpmw.WorkspaceAgent(r) - row, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to get workspace.", - Detail: err.Error(), - }) - return - } - workspace := row.Workspace - - var req agentsdk.Stats - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - // An empty stat means it's just looking for the report interval. - if req.ConnectionsByProto == nil { - httpapi.Write(ctx, rw, http.StatusOK, agentsdk.StatsResponse{ - ReportInterval: api.AgentStatsRefreshInterval, - }) - return - } - - api.Logger.Debug(ctx, "read stats report", - slog.F("interval", api.AgentStatsRefreshInterval), - slog.F("workspace_agent_id", workspaceAgent.ID), - slog.F("workspace_id", workspace.ID), - slog.F("payload", req), - ) - - protoStats := &agentproto.Stats{ - ConnectionsByProto: req.ConnectionsByProto, - ConnectionCount: req.ConnectionCount, - ConnectionMedianLatencyMs: req.ConnectionMedianLatencyMS, - RxPackets: req.RxPackets, - RxBytes: req.RxBytes, - TxPackets: req.TxPackets, - TxBytes: req.TxBytes, - SessionCountVscode: req.SessionCountVSCode, - SessionCountJetbrains: req.SessionCountJetBrains, - SessionCountReconnectingPty: req.SessionCountReconnectingPTY, - SessionCountSsh: req.SessionCountSSH, - Metrics: make([]*agentproto.Stats_Metric, len(req.Metrics)), - } - for i, metric := range req.Metrics { - metricType := agentproto.Stats_Metric_TYPE_UNSPECIFIED - switch metric.Type { - case agentsdk.AgentMetricTypeCounter: - metricType = agentproto.Stats_Metric_COUNTER - case agentsdk.AgentMetricTypeGauge: - metricType = agentproto.Stats_Metric_GAUGE - } - - protoStats.Metrics[i] = &agentproto.Stats_Metric{ - Name: metric.Name, - Type: metricType, - Value: metric.Value, - Labels: make([]*agentproto.Stats_Metric_Label, len(metric.Labels)), - } - for j, label := range metric.Labels { - protoStats.Metrics[i].Labels[j] = &agentproto.Stats_Metric_Label{ - Name: label.Name, - Value: label.Value, - } - } - } - err = api.statsReporter.ReportAgentStats( - ctx, - dbtime.Now(), - workspace, - workspaceAgent, - row.TemplateName, - protoStats, - ) - if err != nil { - httpapi.InternalServerError(rw, err) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, agentsdk.StatsResponse{ - ReportInterval: api.AgentStatsRefreshInterval, - }) -} - -func ellipse(v string, n int) string { - if len(v) > n { - return v[:n] + "..." - } - return v -} - -// @Summary Submit workspace agent metadata -// @ID submit-workspace-agent-metadata -// @Security CoderSessionToken -// @Accept json -// @Tags Agents -// @Param request body []agentsdk.PostMetadataRequest true "Workspace agent metadata request" -// @Success 204 "Success" -// @Router /workspaceagents/me/metadata [post] -// @x-apidocgen {"skip": true} -func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var req agentsdk.PostMetadataRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - workspaceAgent := httpmw.WorkspaceAgent(r) - - // Split into function to allow call by deprecated handler. - err := api.workspaceAgentUpdateMetadata(ctx, workspaceAgent, req) - if err != nil { - api.Logger.Error(ctx, "failed to handle metadata request", slog.Error(err)) - httpapi.InternalServerError(rw, err) - return - } - - httpapi.Write(ctx, rw, http.StatusNoContent, nil) -} - -func (api *API) workspaceAgentUpdateMetadata(ctx context.Context, workspaceAgent database.WorkspaceAgent, req agentsdk.PostMetadataRequest) error { - const ( - // maxValueLen is set to 2048 to stay under the 8000 byte Postgres - // NOTIFY limit. Since both value and error can be set, the real - // payload limit is 2 * 2048 * 4/3 = 5461 bytes + a few hundred bytes for JSON - // syntax, key names, and metadata. - maxValueLen = 2048 - maxErrorLen = maxValueLen - ) - - collectedAt := time.Now() - - datum := database.UpdateWorkspaceAgentMetadataParams{ - WorkspaceAgentID: workspaceAgent.ID, - Key: make([]string, 0, len(req.Metadata)), - Value: make([]string, 0, len(req.Metadata)), - Error: make([]string, 0, len(req.Metadata)), - CollectedAt: make([]time.Time, 0, len(req.Metadata)), - } - - for _, md := range req.Metadata { - metadataError := md.Error - - // We overwrite the error if the provided payload is too long. - if len(md.Value) > maxValueLen { - metadataError = fmt.Sprintf("value of %d bytes exceeded %d bytes", len(md.Value), maxValueLen) - md.Value = md.Value[:maxValueLen] - } - - if len(md.Error) > maxErrorLen { - metadataError = fmt.Sprintf("error of %d bytes exceeded %d bytes", len(md.Error), maxErrorLen) - md.Error = md.Error[:maxErrorLen] - } - - // We don't want a misconfigured agent to fill the database. - datum.Key = append(datum.Key, md.Key) - datum.Value = append(datum.Value, md.Value) - datum.Error = append(datum.Error, metadataError) - // We ignore the CollectedAt from the agent to avoid bugs caused by - // clock skew. - datum.CollectedAt = append(datum.CollectedAt, collectedAt) - - api.Logger.Debug( - ctx, "accepted metadata report", - slog.F("workspace_agent_id", workspaceAgent.ID), - slog.F("collected_at", collectedAt), - slog.F("original_collected_at", md.CollectedAt), - slog.F("key", md.Key), - slog.F("value", ellipse(md.Value, 16)), - ) - } - - payload, err := json.Marshal(agentapi.WorkspaceAgentMetadataChannelPayload{ - CollectedAt: collectedAt, - Keys: datum.Key, - }) - if err != nil { - return err - } - - err = api.Database.UpdateWorkspaceAgentMetadata(ctx, datum) - if err != nil { - return err - } - - err = api.Pubsub.Publish(agentapi.WatchWorkspaceAgentMetadataChannel(workspaceAgent.ID), payload) - if err != nil { - return err - } - - return nil -} - // @Summary Watch for workspace agent metadata updates // @ID watch-for-workspace-agent-metadata-updates // @Security CoderSessionToken @@ -1612,211 +1193,6 @@ func convertWorkspaceAgentMetadata(db []database.WorkspaceAgentMetadatum) []code return result } -// @Summary Submit workspace agent lifecycle state -// @ID submit-workspace-agent-lifecycle-state -// @Security CoderSessionToken -// @Accept json -// @Tags Agents -// @Param request body agentsdk.PostLifecycleRequest true "Workspace agent lifecycle request" -// @Success 204 "Success" -// @Router /workspaceagents/me/report-lifecycle [post] -// @x-apidocgen {"skip": true} -func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - workspaceAgent := httpmw.WorkspaceAgent(r) - row, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to get workspace.", - Detail: err.Error(), - }) - return - } - workspace := row.Workspace - - var req agentsdk.PostLifecycleRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - logger := api.Logger.With( - slog.F("workspace_agent_id", workspaceAgent.ID), - slog.F("workspace_id", workspace.ID), - slog.F("payload", req), - ) - logger.Debug(ctx, "workspace agent state report") - - lifecycleState := req.State - dbLifecycleState := database.WorkspaceAgentLifecycleState(lifecycleState) - if !dbLifecycleState.Valid() { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid lifecycle state.", - Detail: fmt.Sprintf("Invalid lifecycle state %q, must be be one of %q.", lifecycleState, database.AllWorkspaceAgentLifecycleStateValues()), - }) - return - } - - if req.ChangedAt.IsZero() { - // Backwards compatibility with older agents. - req.ChangedAt = dbtime.Now() - } - changedAt := sql.NullTime{Time: req.ChangedAt, Valid: true} - - startedAt := workspaceAgent.StartedAt - readyAt := workspaceAgent.ReadyAt - switch lifecycleState { - case codersdk.WorkspaceAgentLifecycleStarting: - startedAt = changedAt - readyAt.Valid = false // This agent is re-starting, so it's not ready yet. - case codersdk.WorkspaceAgentLifecycleReady, codersdk.WorkspaceAgentLifecycleStartError: - readyAt = changedAt - } - - err = api.Database.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ - ID: workspaceAgent.ID, - LifecycleState: dbLifecycleState, - StartedAt: startedAt, - ReadyAt: readyAt, - }) - if err != nil { - if !xerrors.Is(err, context.Canceled) { - // not an error if we are canceled - logger.Error(ctx, "failed to update lifecycle state", slog.Error(err)) - } - httpapi.InternalServerError(rw, err) - return - } - - api.publishWorkspaceUpdate(ctx, workspace.ID) - - httpapi.Write(ctx, rw, http.StatusNoContent, nil) -} - -// @Summary Submit workspace agent application health -// @ID submit-workspace-agent-application-health -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Agents -// @Param request body agentsdk.PostAppHealthsRequest true "Application health request" -// @Success 200 -// @Router /workspaceagents/me/app-health [post] -func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgent(r) - var req agentsdk.PostAppHealthsRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - if req.Healths == nil || len(req.Healths) == 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Health field is empty", - }) - return - } - - apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Error getting agent apps", - Detail: err.Error(), - }) - return - } - - var newApps []database.WorkspaceApp - for id, newHealth := range req.Healths { - old := func() *database.WorkspaceApp { - for _, app := range apps { - if app.ID == id { - return &app - } - } - - return nil - }() - if old == nil { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: "Error setting workspace app health", - Detail: xerrors.Errorf("workspace app name %s not found", id).Error(), - }) - return - } - - if old.HealthcheckUrl == "" { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: "Error setting workspace app health", - Detail: xerrors.Errorf("health checking is disabled for workspace app %s", id).Error(), - }) - return - } - - switch newHealth { - case codersdk.WorkspaceAppHealthInitializing: - case codersdk.WorkspaceAppHealthHealthy: - case codersdk.WorkspaceAppHealthUnhealthy: - default: - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Error setting workspace app health", - Detail: xerrors.Errorf("workspace app health %s is not a valid value", newHealth).Error(), - }) - return - } - - // don't save if the value hasn't changed - if old.Health == database.WorkspaceAppHealth(newHealth) { - continue - } - old.Health = database.WorkspaceAppHealth(newHealth) - - newApps = append(newApps, *old) - } - - for _, app := range newApps { - err = api.Database.UpdateWorkspaceAppHealthByID(ctx, database.UpdateWorkspaceAppHealthByIDParams{ - ID: app.ID, - Health: app.Health, - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Error setting workspace app health", - Detail: err.Error(), - }) - return - } - } - - resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource.", - Detail: err.Error(), - }) - return - } - job, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build.", - Detail: err.Error(), - }) - return - } - workspace, err := api.Database.GetWorkspaceByID(ctx, job.WorkspaceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), - }) - return - } - api.publishWorkspaceUpdate(ctx, workspace.ID) - - httpapi.Write(ctx, rw, http.StatusOK, nil) -} - // workspaceAgentsExternalAuth returns an access token for a given URL // or finds a provider by ID. // @@ -2117,24 +1493,3 @@ func convertWorkspaceAgentLog(logEntry database.WorkspaceAgentLog) codersdk.Work SourceID: logEntry.LogSourceID, } } - -func convertWorkspaceAgentSubsystems(ss []codersdk.AgentSubsystem) []database.WorkspaceAgentSubsystem { - out := make([]database.WorkspaceAgentSubsystem, 0, len(ss)) - for _, s := range ss { - switch s { - case codersdk.AgentSubsystemEnvbox: - out = append(out, database.WorkspaceAgentSubsystemEnvbox) - case codersdk.AgentSubsystemEnvbuilder: - out = append(out, database.WorkspaceAgentSubsystemEnvbuilder) - case codersdk.AgentSubsystemExectrace: - out = append(out, database.WorkspaceAgentSubsystemExectrace) - default: - // Invalid, drop it. - } - } - - sort.Slice(out, func(i, j int) bool { - return out[i] < out[j] - }) - return out -} diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 7052d59144e1b..f205962b7eb62 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -34,7 +34,6 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" - "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -963,105 +962,6 @@ func TestWorkspaceAgentPostLogSource(t *testing.T) { }) } -// TestWorkspaceAgentReportStats tests the legacy (agent API v1) report stats endpoint. -func TestWorkspaceAgentReportStats(t *testing.T) { - t.Parallel() - - t.Run("OK", func(t *testing.T) { - t.Parallel() - - client, db := coderdtest.NewWithDatabase(t, nil) - user := coderdtest.CreateFirstUser(t, client) - r := dbfake.WorkspaceBuild(t, db, database.Workspace{ - OrganizationID: user.OrganizationID, - OwnerID: user.UserID, - }).WithAgent().Do() - - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) - - _, err := agentClient.PostStats(context.Background(), &agentsdk.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVSCode: 1, - SessionCountJetBrains: 0, - SessionCountReconnectingPTY: 0, - SessionCountSSH: 0, - ConnectionMedianLatencyMS: 10, - }) - require.NoError(t, err) - - newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) - require.NoError(t, err) - - assert.True(t, - newWorkspace.LastUsedAt.After(r.Workspace.LastUsedAt), - "%s is not after %s", newWorkspace.LastUsedAt, r.Workspace.LastUsedAt, - ) - }) - - t.Run("FailDeleted", func(t *testing.T) { - t.Parallel() - - owner, db := coderdtest.NewWithDatabase(t, nil) - ownerUser := coderdtest.CreateFirstUser(t, owner) - client, admin := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) - r := dbfake.WorkspaceBuild(t, db, database.Workspace{ - OrganizationID: admin.OrganizationIDs[0], - OwnerID: admin.ID, - }).WithAgent().Do() - - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) - - _, err := agentClient.PostStats(context.Background(), &agentsdk.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVSCode: 0, - SessionCountJetBrains: 0, - SessionCountReconnectingPTY: 0, - SessionCountSSH: 0, - ConnectionMedianLatencyMS: 10, - }) - require.NoError(t, err) - - newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) - require.NoError(t, err) - - // nolint:gocritic // using db directly over creating a delete job - err = db.UpdateWorkspaceDeletedByID(dbauthz.As(context.Background(), - coderdtest.AuthzUserSubject(admin, ownerUser.OrganizationID)), - database.UpdateWorkspaceDeletedByIDParams{ - ID: newWorkspace.ID, - Deleted: true, - }) - require.NoError(t, err) - - _, err = agentClient.PostStats(context.Background(), &agentsdk.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVSCode: 1, - SessionCountJetBrains: 0, - SessionCountReconnectingPTY: 0, - SessionCountSSH: 0, - ConnectionMedianLatencyMS: 10, - }) - require.ErrorContains(t, err, "agent is invalid") - }) -} - func TestWorkspaceAgent_LifecycleState(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index 24b6088ddd8f2..ec8dcd8a0e3fc 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -164,31 +164,6 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { } } -func (api *API) startAgentWebsocketMonitor(ctx context.Context, - workspaceAgent database.WorkspaceAgent, workspaceBuild database.WorkspaceBuild, - conn *websocket.Conn, -) *agentConnectionMonitor { - monitor := &agentConnectionMonitor{ - apiCtx: api.ctx, - workspaceAgent: workspaceAgent, - workspaceBuild: workspaceBuild, - conn: conn, - pingPeriod: api.AgentConnectionUpdateFrequency, - db: api.Database, - replicaID: api.ID, - updater: api, - disconnectTimeout: api.AgentInactiveDisconnectTimeout, - logger: api.Logger.With( - slog.F("workspace_id", workspaceBuild.WorkspaceID), - slog.F("agent_id", workspaceAgent.ID), - ), - } - monitor.init() - monitor.start(ctx) - - return monitor -} - type yamuxPingerCloser struct { mux *yamux.Session } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index eb76239b84658..5d99e56820aa1 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -728,7 +728,7 @@ func TestWorkspaceDeleteSuspendedUser(t *testing.T) { validateCalls++ if userSuspended { // Simulate the user being suspended from the IDP too. - return "", http.StatusForbidden, fmt.Errorf("user is suspended") + return "", http.StatusForbidden, xerrors.New("user is suspended") } return "OK", 0, nil }, From d6b4d99efff42d595e4f12ae4461f52bb5ed1584 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 04:42:44 +0000 Subject: [PATCH 2/8] workspaceagents_test rest -> drpc --- coderd/workspaceagents_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index f205962b7eb62..153a4135d3082 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1097,11 +1097,11 @@ func TestWorkspaceAgent_Metadata(t *testing.T) { require.EqualValues(t, 3, manifest.Metadata[0].Timeout) post := func(ctx context.Context, key string, mr codersdk.WorkspaceAgentMetadataResult) { - err := agentClient.PostMetadata(ctx, agentsdk.PostMetadataRequest{ - Metadata: []agentsdk.Metadata{ + _, err := aAPI.BatchUpdateMetadata(ctx, &agentproto.BatchUpdateMetadataRequest{ + Metadata: []*agentproto.Metadata{ { - Key: key, - WorkspaceAgentMetadataResult: mr, + Key: key, + Result: agentsdk.ProtoFromMetadataResult(mr), }, }, }) @@ -1352,17 +1352,18 @@ func TestWorkspaceAgent_Metadata_CatchMemoryLeak(t *testing.T) { manifest := requireGetManifest(ctx, t, aAPI) post := func(ctx context.Context, key, value string) error { - return agentClient.PostMetadata(ctx, agentsdk.PostMetadataRequest{ - Metadata: []agentsdk.Metadata{ + _, err := aAPI.BatchUpdateMetadata(ctx, &agentproto.BatchUpdateMetadataRequest{ + Metadata: []*agentproto.Metadata{ { Key: key, - WorkspaceAgentMetadataResult: codersdk.WorkspaceAgentMetadataResult{ + Result: agentsdk.ProtoFromMetadataResult(codersdk.WorkspaceAgentMetadataResult{ CollectedAt: time.Now(), Value: value, - }, + }), }, }, }) + return err } workspace, err = client.Workspace(ctx, workspace.ID) From 48b31d38598229616382c80efdaee1f18eb46fbc Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 05:52:22 +0000 Subject: [PATCH 3/8] new api tests + gen --- coderd/apidoc/docs.go | 646 +----------------- coderd/apidoc/swagger.json | 566 --------------- .../insights/metricscollector_test.go | 211 ------ .../prometheusmetrics_test.go | 57 +- coderd/workspaceagents_test.go | 22 +- codersdk/agentsdk/agentsdk.go | 55 -- codersdk/agentsdk/convert.go | 8 + docs/api/agents.md | 338 --------- docs/api/schemas.md | 411 ----------- 9 files changed, 68 insertions(+), 2246 deletions(-) delete mode 100644 coderd/prometheusmetrics/insights/metricscollector_test.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c5e2a6041526f..f4cddf15cf21b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5696,62 +5696,6 @@ const docTemplate = `{ } } }, - "/workspaceagents/me/app-health": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Submit workspace agent application health", - "operationId": "submit-workspace-agent-application-health", - "parameters": [ - { - "description": "Application health request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostAppHealthsRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/workspaceagents/me/coordinate": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "description": "It accepts a WebSocket connection to an agent that listens to\nincoming connections and publishes node updates.", - "tags": [ - "Agents" - ], - "summary": "Coordinate workspace agent via Tailnet", - "operationId": "coordinate-workspace-agent-via-tailnet", - "responses": { - "101": { - "description": "Switching Protocols" - } - } - } - }, "/workspaceagents/me/external-auth": { "get": { "security": [ @@ -5949,287 +5893,25 @@ const docTemplate = `{ } } }, - "/workspaceagents/me/manifest": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Get authorized workspace agent manifest", - "operationId": "get-authorized-workspace-agent-manifest", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/agentsdk.Manifest" - } - } - } - } - }, - "/workspaceagents/me/metadata": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Submit workspace agent metadata", - "operationId": "submit-workspace-agent-metadata", - "parameters": [ - { - "description": "Workspace agent metadata request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.PostMetadataRequest" - } - } - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/metadata/{key}": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Removed: Submit workspace agent metadata", - "operationId": "removed-submit-workspace-agent-metadata", - "parameters": [ - { - "description": "Workspace agent metadata request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostMetadataRequestDeprecated" - } - }, - { - "type": "string", - "format": "string", - "description": "metadata key", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/report-lifecycle": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Submit workspace agent lifecycle state", - "operationId": "submit-workspace-agent-lifecycle-state", - "parameters": [ - { - "description": "Workspace agent lifecycle request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostLifecycleRequest" - } - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/report-stats": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Submit workspace agent stats", - "operationId": "submit-workspace-agent-stats", - "deprecated": true, - "parameters": [ - { - "description": "Stats request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.Stats" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/agentsdk.StatsResponse" - } - } - } - } - }, "/workspaceagents/me/rpc": { "get": { "security": [ { - "CoderSessionToken": [] - } - ], - "tags": [ - "Agents" - ], - "summary": "Workspace agent RPC API", - "operationId": "workspace-agent-rpc-api", - "responses": { - "101": { - "description": "Switching Protocols" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/startup": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Submit workspace agent startup", - "operationId": "submit-workspace-agent-startup", - "parameters": [ - { - "description": "Startup request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostStartupRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/startup-logs": { - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Removed: Patch workspace agent logs", - "operationId": "removed-patch-workspace-agent-logs", - "parameters": [ - { - "description": "logs", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PatchLogs" - } + "CoderSessionToken": [] } ], + "tags": [ + "Agents" + ], + "summary": "Workspace agent RPC API", + "operationId": "workspace-agent-rpc-api", "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } + "101": { + "description": "Switching Protocols" } + }, + "x-apidocgen": { + "skip": true } } }, @@ -7882,65 +7564,6 @@ const docTemplate = `{ } } }, - "agentsdk.AgentMetric": { - "type": "object", - "required": [ - "name", - "type", - "value" - ], - "properties": { - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.AgentMetricLabel" - } - }, - "name": { - "type": "string" - }, - "type": { - "enum": [ - "counter", - "gauge" - ], - "allOf": [ - { - "$ref": "#/definitions/agentsdk.AgentMetricType" - } - ] - }, - "value": { - "type": "number" - } - } - }, - "agentsdk.AgentMetricLabel": { - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "agentsdk.AgentMetricType": { - "type": "string", - "enum": [ - "counter", - "gauge" - ], - "x-enum-varnames": [ - "AgentMetricTypeCounter", - "AgentMetricTypeGauge" - ] - }, "agentsdk.AuthenticateResponse": { "type": "object", "properties": { @@ -8025,95 +7648,6 @@ const docTemplate = `{ } } }, - "agentsdk.Manifest": { - "type": "object", - "properties": { - "agent_id": { - "type": "string" - }, - "agent_name": { - "type": "string" - }, - "apps": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceApp" - } - }, - "derp_force_websockets": { - "type": "boolean" - }, - "derpmap": { - "$ref": "#/definitions/tailcfg.DERPMap" - }, - "directory": { - "type": "string" - }, - "disable_direct_connections": { - "type": "boolean" - }, - "environment_variables": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "git_auth_configs": { - "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", - "type": "integer" - }, - "metadata": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" - } - }, - "motd_file": { - "type": "string" - }, - "owner_name": { - "description": "OwnerName and WorkspaceID are used by an open-source user to identify the workspace.\nWe do not provide insurance that this will not be removed in the future,\nbut if it's easy to persist lets keep it around.", - "type": "string" - }, - "scripts": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentScript" - } - }, - "vscode_port_proxy_uri": { - "type": "string" - }, - "workspace_id": { - "type": "string" - }, - "workspace_name": { - "type": "string" - } - } - }, - "agentsdk.Metadata": { - "type": "object", - "properties": { - "age": { - "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", - "type": "integer" - }, - "collected_at": { - "type": "string", - "format": "date-time" - }, - "error": { - "type": "string" - }, - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, "agentsdk.PatchLogs": { "type": "object", "properties": { @@ -8128,29 +7662,6 @@ const docTemplate = `{ } } }, - "agentsdk.PostAppHealthsRequest": { - "type": "object", - "properties": { - "healths": { - "description": "Healths is a map of the workspace app name and the health of the app.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAppHealth" - } - } - } - }, - "agentsdk.PostLifecycleRequest": { - "type": "object", - "properties": { - "changed_at": { - "type": "string" - }, - "state": { - "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" - } - } - }, "agentsdk.PostLogSourceRequest": { "type": "object", "properties": { @@ -8166,121 +7677,6 @@ const docTemplate = `{ } } }, - "agentsdk.PostMetadataRequest": { - "type": "object", - "properties": { - "metadata": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.Metadata" - } - } - } - }, - "agentsdk.PostMetadataRequestDeprecated": { - "type": "object", - "properties": { - "age": { - "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", - "type": "integer" - }, - "collected_at": { - "type": "string", - "format": "date-time" - }, - "error": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "agentsdk.PostStartupRequest": { - "type": "object", - "properties": { - "expanded_directory": { - "type": "string" - }, - "subsystems": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AgentSubsystem" - } - }, - "version": { - "type": "string" - } - } - }, - "agentsdk.Stats": { - "type": "object", - "properties": { - "connection_count": { - "description": "ConnectionCount is the number of connections received by an agent.", - "type": "integer" - }, - "connection_median_latency_ms": { - "description": "ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.", - "type": "number" - }, - "connections_by_proto": { - "description": "ConnectionsByProto is a count of connections by protocol.", - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "metrics": { - "description": "Metrics collected by the agent", - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.AgentMetric" - } - }, - "rx_bytes": { - "description": "RxBytes is the number of received bytes.", - "type": "integer" - }, - "rx_packets": { - "description": "RxPackets is the number of received packets.", - "type": "integer" - }, - "session_count_jetbrains": { - "description": "SessionCountJetBrains is the number of connections received by an agent\nthat are from our JetBrains extension.", - "type": "integer" - }, - "session_count_reconnecting_pty": { - "description": "SessionCountReconnectingPTY is the number of connections received by an agent\nthat are from the reconnecting web terminal.", - "type": "integer" - }, - "session_count_ssh": { - "description": "SessionCountSSH is the number of connections received by an agent\nthat are normal, non-tagged SSH sessions.", - "type": "integer" - }, - "session_count_vscode": { - "description": "SessionCountVSCode is the number of connections received by an agent\nthat are from our VS Code extension.", - "type": "integer" - }, - "tx_bytes": { - "description": "TxBytes is the number of transmitted bytes.", - "type": "integer" - }, - "tx_packets": { - "description": "TxPackets is the number of transmitted bytes.", - "type": "integer" - } - } - }, - "agentsdk.StatsResponse": { - "type": "object", - "properties": { - "report_interval": { - "description": "ReportInterval is the duration after which the agent should send stats\nagain.", - "type": "integer" - } - } - }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -13141,26 +12537,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentMetadataDescription": { - "type": "object", - "properties": { - "display_name": { - "type": "string" - }, - "interval": { - "type": "integer" - }, - "key": { - "type": "string" - }, - "script": { - "type": "string" - }, - "timeout": { - "type": "integer" - } - } - }, "codersdk.WorkspaceAgentPortShare": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 66afad1f041f0..fe98dc5c6b304 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5024,54 +5024,6 @@ } } }, - "/workspaceagents/me/app-health": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Submit workspace agent application health", - "operationId": "submit-workspace-agent-application-health", - "parameters": [ - { - "description": "Application health request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostAppHealthsRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/workspaceagents/me/coordinate": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "description": "It accepts a WebSocket connection to an agent that listens to\nincoming connections and publishes node updates.", - "tags": ["Agents"], - "summary": "Coordinate workspace agent via Tailnet", - "operationId": "coordinate-workspace-agent-via-tailnet", - "responses": { - "101": { - "description": "Switching Protocols" - } - } - } - }, "/workspaceagents/me/external-auth": { "get": { "security": [ @@ -5245,168 +5197,6 @@ } } }, - "/workspaceagents/me/manifest": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Get authorized workspace agent manifest", - "operationId": "get-authorized-workspace-agent-manifest", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/agentsdk.Manifest" - } - } - } - } - }, - "/workspaceagents/me/metadata": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "tags": ["Agents"], - "summary": "Submit workspace agent metadata", - "operationId": "submit-workspace-agent-metadata", - "parameters": [ - { - "description": "Workspace agent metadata request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.PostMetadataRequest" - } - } - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/metadata/{key}": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "tags": ["Agents"], - "summary": "Removed: Submit workspace agent metadata", - "operationId": "removed-submit-workspace-agent-metadata", - "parameters": [ - { - "description": "Workspace agent metadata request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostMetadataRequestDeprecated" - } - }, - { - "type": "string", - "format": "string", - "description": "metadata key", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/report-lifecycle": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "tags": ["Agents"], - "summary": "Submit workspace agent lifecycle state", - "operationId": "submit-workspace-agent-lifecycle-state", - "parameters": [ - { - "description": "Workspace agent lifecycle request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostLifecycleRequest" - } - } - ], - "responses": { - "204": { - "description": "Success" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/report-stats": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Submit workspace agent stats", - "operationId": "submit-workspace-agent-stats", - "deprecated": true, - "parameters": [ - { - "description": "Stats request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.Stats" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/agentsdk.StatsResponse" - } - } - } - } - }, "/workspaceagents/me/rpc": { "get": { "security": [ @@ -5427,72 +5217,6 @@ } } }, - "/workspaceagents/me/startup": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Submit workspace agent startup", - "operationId": "submit-workspace-agent-startup", - "parameters": [ - { - "description": "Startup request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PostStartupRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - }, - "x-apidocgen": { - "skip": true - } - } - }, - "/workspaceagents/me/startup-logs": { - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Removed: Patch workspace agent logs", - "operationId": "removed-patch-workspace-agent-logs", - "parameters": [ - { - "description": "logs", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PatchLogs" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } - } - } - } - }, "/workspaceagents/{workspaceagent}": { "get": { "security": [ @@ -6969,49 +6693,6 @@ } } }, - "agentsdk.AgentMetric": { - "type": "object", - "required": ["name", "type", "value"], - "properties": { - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.AgentMetricLabel" - } - }, - "name": { - "type": "string" - }, - "type": { - "enum": ["counter", "gauge"], - "allOf": [ - { - "$ref": "#/definitions/agentsdk.AgentMetricType" - } - ] - }, - "value": { - "type": "number" - } - } - }, - "agentsdk.AgentMetricLabel": { - "type": "object", - "required": ["name", "value"], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "agentsdk.AgentMetricType": { - "type": "string", - "enum": ["counter", "gauge"], - "x-enum-varnames": ["AgentMetricTypeCounter", "AgentMetricTypeGauge"] - }, "agentsdk.AuthenticateResponse": { "type": "object", "properties": { @@ -7091,95 +6772,6 @@ } } }, - "agentsdk.Manifest": { - "type": "object", - "properties": { - "agent_id": { - "type": "string" - }, - "agent_name": { - "type": "string" - }, - "apps": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceApp" - } - }, - "derp_force_websockets": { - "type": "boolean" - }, - "derpmap": { - "$ref": "#/definitions/tailcfg.DERPMap" - }, - "directory": { - "type": "string" - }, - "disable_direct_connections": { - "type": "boolean" - }, - "environment_variables": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "git_auth_configs": { - "description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.", - "type": "integer" - }, - "metadata": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" - } - }, - "motd_file": { - "type": "string" - }, - "owner_name": { - "description": "OwnerName and WorkspaceID are used by an open-source user to identify the workspace.\nWe do not provide insurance that this will not be removed in the future,\nbut if it's easy to persist lets keep it around.", - "type": "string" - }, - "scripts": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentScript" - } - }, - "vscode_port_proxy_uri": { - "type": "string" - }, - "workspace_id": { - "type": "string" - }, - "workspace_name": { - "type": "string" - } - } - }, - "agentsdk.Metadata": { - "type": "object", - "properties": { - "age": { - "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", - "type": "integer" - }, - "collected_at": { - "type": "string", - "format": "date-time" - }, - "error": { - "type": "string" - }, - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, "agentsdk.PatchLogs": { "type": "object", "properties": { @@ -7194,29 +6786,6 @@ } } }, - "agentsdk.PostAppHealthsRequest": { - "type": "object", - "properties": { - "healths": { - "description": "Healths is a map of the workspace app name and the health of the app.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAppHealth" - } - } - } - }, - "agentsdk.PostLifecycleRequest": { - "type": "object", - "properties": { - "changed_at": { - "type": "string" - }, - "state": { - "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" - } - } - }, "agentsdk.PostLogSourceRequest": { "type": "object", "properties": { @@ -7232,121 +6801,6 @@ } } }, - "agentsdk.PostMetadataRequest": { - "type": "object", - "properties": { - "metadata": { - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.Metadata" - } - } - } - }, - "agentsdk.PostMetadataRequestDeprecated": { - "type": "object", - "properties": { - "age": { - "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", - "type": "integer" - }, - "collected_at": { - "type": "string", - "format": "date-time" - }, - "error": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "agentsdk.PostStartupRequest": { - "type": "object", - "properties": { - "expanded_directory": { - "type": "string" - }, - "subsystems": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AgentSubsystem" - } - }, - "version": { - "type": "string" - } - } - }, - "agentsdk.Stats": { - "type": "object", - "properties": { - "connection_count": { - "description": "ConnectionCount is the number of connections received by an agent.", - "type": "integer" - }, - "connection_median_latency_ms": { - "description": "ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.", - "type": "number" - }, - "connections_by_proto": { - "description": "ConnectionsByProto is a count of connections by protocol.", - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "metrics": { - "description": "Metrics collected by the agent", - "type": "array", - "items": { - "$ref": "#/definitions/agentsdk.AgentMetric" - } - }, - "rx_bytes": { - "description": "RxBytes is the number of received bytes.", - "type": "integer" - }, - "rx_packets": { - "description": "RxPackets is the number of received packets.", - "type": "integer" - }, - "session_count_jetbrains": { - "description": "SessionCountJetBrains is the number of connections received by an agent\nthat are from our JetBrains extension.", - "type": "integer" - }, - "session_count_reconnecting_pty": { - "description": "SessionCountReconnectingPTY is the number of connections received by an agent\nthat are from the reconnecting web terminal.", - "type": "integer" - }, - "session_count_ssh": { - "description": "SessionCountSSH is the number of connections received by an agent\nthat are normal, non-tagged SSH sessions.", - "type": "integer" - }, - "session_count_vscode": { - "description": "SessionCountVSCode is the number of connections received by an agent\nthat are from our VS Code extension.", - "type": "integer" - }, - "tx_bytes": { - "description": "TxBytes is the number of transmitted bytes.", - "type": "integer" - }, - "tx_packets": { - "description": "TxPackets is the number of transmitted bytes.", - "type": "integer" - } - } - }, - "agentsdk.StatsResponse": { - "type": "object", - "properties": { - "report_interval": { - "description": "ReportInterval is the duration after which the agent should send stats\nagain.", - "type": "integer" - } - } - }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -11943,26 +11397,6 @@ } } }, - "codersdk.WorkspaceAgentMetadataDescription": { - "type": "object", - "properties": { - "display_name": { - "type": "string" - }, - "interval": { - "type": "integer" - }, - "key": { - "type": "string" - }, - "script": { - "type": "string" - }, - "timeout": { - "type": "integer" - } - } - }, "codersdk.WorkspaceAgentPortShare": { "type": "object", "properties": { diff --git a/coderd/prometheusmetrics/insights/metricscollector_test.go b/coderd/prometheusmetrics/insights/metricscollector_test.go deleted file mode 100644 index 91ef3c7ee88fa..0000000000000 --- a/coderd/prometheusmetrics/insights/metricscollector_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package insights_test - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/prometheus/client_golang/prometheus" - io_prometheus_client "github.com/prometheus/client_model/go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbauthz" - "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/coderd/database/dbtestutil" - "github.com/coder/coder/v2/coderd/prometheusmetrics/insights" - "github.com/coder/coder/v2/coderd/workspaceapps" - "github.com/coder/coder/v2/coderd/workspacestats" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/testutil" -) - -func TestCollectInsights(t *testing.T) { - t.Parallel() - - logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) - db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) - - options := &coderdtest.Options{ - IncludeProvisionerDaemon: true, - AgentStatsRefreshInterval: time.Millisecond * 100, - Database: db, - Pubsub: ps, - } - ownerClient := coderdtest.New(t, options) - ownerClient.SetLogger(logger.Named("ownerClient").Leveled(slog.LevelDebug)) - owner := coderdtest.CreateFirstUser(t, ownerClient) - client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) - - // Given - // Initialize metrics collector - mc, err := insights.NewMetricsCollector(db, logger, 0, time.Second) - require.NoError(t, err) - - registry := prometheus.NewRegistry() - registry.Register(mc) - - var ( - orgID = owner.OrganizationID - tpl = dbgen.Template(t, db, database.Template{OrganizationID: orgID, CreatedBy: user.ID, Name: "golden-template"}) - ver = dbgen.TemplateVersion(t, db, database.TemplateVersion{OrganizationID: orgID, CreatedBy: user.ID, TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}}) - param1 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "first_parameter"}) - param2 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "second_parameter", Type: "bool"}) - param3 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "third_parameter", Type: "number"}) - workspace1 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID}) - workspace2 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID}) - job1 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID}) - job2 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID}) - build1 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace1.ID, JobID: job1.ID}) - build2 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace2.ID, JobID: job2.ID}) - res1 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build1.JobID}) - res2 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build2.JobID}) - agent1 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res1.ID}) - agent2 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res2.ID}) - app1 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent1.ID, Slug: "golden-slug", DisplayName: "Golden Slug"}) - app2 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent2.ID, Slug: "golden-slug", DisplayName: "Golden Slug"}) - _ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ - {WorkspaceBuildID: build1.ID, Name: param1.Name, Value: "Foobar"}, - {WorkspaceBuildID: build1.ID, Name: param2.Name, Value: "true"}, - {WorkspaceBuildID: build1.ID, Name: param3.Name, Value: "789"}, - }) - _ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ - {WorkspaceBuildID: build2.ID, Name: param1.Name, Value: "Baz"}, - {WorkspaceBuildID: build2.ID, Name: param2.Name, Value: "true"}, - {WorkspaceBuildID: build2.ID, Name: param3.Name, Value: "999"}, - }) - ) - - // Start an agent so that we can generate stats. - var agentClients []*agentsdk.Client - for i, agent := range []database.WorkspaceAgent{agent1, agent2} { - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(agent.AuthToken.String()) - agentClient.SDK.SetLogger(logger.Leveled(slog.LevelDebug).Named(fmt.Sprintf("agent%d", i+1))) - agentClients = append(agentClients, agentClient) - } - - // Fake app stats - _, err = agentClients[0].PostStats(context.Background(), &agentsdk.Stats{ - // ConnectionCount must be positive as database query ignores stats with no active connections at the time frame - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - ConnectionMedianLatencyMS: 15, - // Session counts must be positive, but the exact value is ignored. - // Database query approximates it to 60s of usage. - SessionCountSSH: 99, - SessionCountJetBrains: 47, - SessionCountVSCode: 34, - }) - require.NoError(t, err, "unable to post fake stats") - - // Fake app usage - reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ - Database: db, - AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, - }) - refTime := time.Now().Add(-3 * time.Minute).Truncate(time.Minute) - //nolint:gocritic // This is a test. - err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(context.Background()), []workspaceapps.StatsReport{ - { - UserID: user.ID, - WorkspaceID: workspace1.ID, - AgentID: agent1.ID, - AccessMethod: "path", - SlugOrPort: app1.Slug, - SessionID: uuid.New(), - SessionStartedAt: refTime, - SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second), - Requests: 1, - }, - // Same usage on differrent workspace/agent in same template, - // should not be counted as extra. - { - UserID: user.ID, - WorkspaceID: workspace2.ID, - AgentID: agent2.ID, - AccessMethod: "path", - SlugOrPort: app2.Slug, - SessionID: uuid.New(), - SessionStartedAt: refTime, - SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second), - Requests: 1, - }, - { - UserID: user.ID, - WorkspaceID: workspace2.ID, - AgentID: agent2.ID, - AccessMethod: "path", - SlugOrPort: app2.Slug, - SessionID: uuid.New(), - SessionStartedAt: refTime.Add(2 * time.Minute), - SessionEndedAt: refTime.Add(2 * time.Minute).Add(30 * time.Second), - Requests: 1, - }, - }) - require.NoError(t, err, "want no error inserting app stats") - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - // Run metrics collector - closeFunc, err := mc.Run(ctx) - require.NoError(t, err) - defer closeFunc() - - goldenFile, err := os.ReadFile("testdata/insights-metrics.json") - require.NoError(t, err) - golden := map[string]int{} - err = json.Unmarshal(goldenFile, &golden) - require.NoError(t, err) - - collected := map[string]int{} - ok := assert.Eventuallyf(t, func() bool { - // When - metrics, err := registry.Gather() - if !assert.NoError(t, err) { - return false - } - - // Then - for _, metric := range metrics { - t.Logf("metric: %s: %#v", metric.GetName(), metric) - switch metric.GetName() { - case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users", "coderd_insights_parameters": - for _, m := range metric.Metric { - key := metric.GetName() - if len(m.Label) > 0 { - key = key + "[" + metricLabelAsString(m) + "]" - } - collected[key] = int(m.Gauge.GetValue()) - } - default: - assert.Failf(t, "unexpected metric collected", "metric: %s", metric.GetName()) - } - } - - return assert.ObjectsAreEqualValues(golden, collected) - }, testutil.WaitMedium, testutil.IntervalFast, "template insights are inconsistent with golden files") - if !ok { - diff := cmp.Diff(golden, collected) - assert.Empty(t, diff, "template insights are inconsistent with golden files (-golden +collected)") - } -} - -func metricLabelAsString(m *io_prometheus_client.Metric) string { - var labels []string - for _, labelPair := range m.Label { - labels = append(labels, labelPair.GetName()+"="+labelPair.GetValue()) - } - return strings.Join(labels, ",") -} diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index 9c4c9fca0b66f..e76383b8b3fa3 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -20,6 +20,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentmetrics" "github.com/coder/coder/v2/coderd/batchstats" "github.com/coder/coder/v2/coderd/coderdtest" @@ -415,36 +416,45 @@ func TestAgentStats(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) - agent1 := prepareWorkspaceAndAgent(t, client, user, 1) - agent2 := prepareWorkspaceAndAgent(t, client, user, 2) - agent3 := prepareWorkspaceAndAgent(t, client, user, 3) + agent1 := prepareWorkspaceAndAgent(ctx, t, client, user, 1) + agent2 := prepareWorkspaceAndAgent(ctx, t, client, user, 2) + agent3 := prepareWorkspaceAndAgent(ctx, t, client, user, 3) + defer agent1.DRPCConn().Close() + defer agent2.DRPCConn().Close() + defer agent3.DRPCConn().Close() registry := prometheus.NewRegistry() // given var i int64 for i = 0; i < 3; i++ { - _, err = agent1.PostStats(ctx, &agentsdk.Stats{ - TxBytes: 1 + i, RxBytes: 2 + i, - SessionCountVSCode: 3 + i, SessionCountJetBrains: 4 + i, SessionCountReconnectingPTY: 5 + i, SessionCountSSH: 6 + i, - ConnectionCount: 7 + i, ConnectionMedianLatencyMS: 8000, - ConnectionsByProto: map[string]int64{"TCP": 1}, + _, err = agent1.UpdateStats(ctx, &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + TxBytes: 1 + i, RxBytes: 2 + i, + SessionCountVscode: 3 + i, SessionCountJetbrains: 4 + i, SessionCountReconnectingPty: 5 + i, SessionCountSsh: 6 + i, + ConnectionCount: 7 + i, ConnectionMedianLatencyMs: 8000, + ConnectionsByProto: map[string]int64{"TCP": 1}, + }, }) require.NoError(t, err) - _, err = agent2.PostStats(ctx, &agentsdk.Stats{ - TxBytes: 2 + i, RxBytes: 4 + i, - SessionCountVSCode: 6 + i, SessionCountJetBrains: 8 + i, SessionCountReconnectingPTY: 10 + i, SessionCountSSH: 12 + i, - ConnectionCount: 8 + i, ConnectionMedianLatencyMS: 10000, - ConnectionsByProto: map[string]int64{"TCP": 1}, + _, err = agent2.UpdateStats(ctx, &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + TxBytes: 2 + i, RxBytes: 4 + i, + SessionCountVscode: 6 + i, SessionCountJetbrains: 8 + i, SessionCountReconnectingPty: 10 + i, SessionCountSsh: 12 + i, + ConnectionCount: 8 + i, ConnectionMedianLatencyMs: 10000, + ConnectionsByProto: map[string]int64{"TCP": 1}, + }, }) require.NoError(t, err) - _, err = agent3.PostStats(ctx, &agentsdk.Stats{ - TxBytes: 3 + i, RxBytes: 6 + i, - SessionCountVSCode: 12 + i, SessionCountJetBrains: 14 + i, SessionCountReconnectingPTY: 16 + i, SessionCountSSH: 18 + i, - ConnectionCount: 9 + i, ConnectionMedianLatencyMS: 12000, - ConnectionsByProto: map[string]int64{"TCP": 1}, + _, err = agent3.UpdateStats(ctx, &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + TxBytes: 3 + i, RxBytes: 6 + i, + SessionCountVscode: 12 + i, SessionCountJetbrains: 14 + i, SessionCountReconnectingPty: 16 + i, SessionCountSsh: 18 + i, + ConnectionCount: 9 + i, ConnectionMedianLatencyMs: 12000, + ConnectionsByProto: map[string]int64{"TCP": 1}, + }, }) require.NoError(t, err) } @@ -596,7 +606,7 @@ func TestExperimentsMetric(t *testing.T) { } } -func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, workspaceNum int) *agentsdk.Client { +func prepareWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, workspaceNum int) agentproto.DRPCAgentClient { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ @@ -611,9 +621,12 @@ func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user coders }) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(authToken) - return agentClient + ac := agentsdk.New(client.URL) + ac.SetSessionToken(authToken) + conn, err := ac.ConnectRPC(ctx) + require.NoError(t, err) + agentAPI := agentproto.NewDRPCAgentClient(conn) + return agentAPI } var ( diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 153a4135d3082..aed4e63349040 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/timestamppb" "tailscale.com/tailcfg" "cdr.dev/slog" @@ -967,6 +968,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { t.Run("Set", func(t *testing.T) { t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) client, db := coderdtest.NewWithDatabase(t, nil) user := coderdtest.CreateFirstUser(t, client) @@ -982,8 +984,11 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { } } - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) + ac := agentsdk.New(client.URL) + ac.SetSessionToken(r.AgentToken) + conn, err := ac.ConnectRPC(ctx) + require.NoError(t, err) + agentAPI := agentproto.NewDRPCAgentClient(conn) tests := []struct { state codersdk.WorkspaceAgentLifecycle @@ -1005,16 +1010,17 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { for _, tt := range tests { tt := tt t.Run(string(tt.state), func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - - err := agentClient.PostLifecycle(ctx, agentsdk.PostLifecycleRequest{ - State: tt.state, - ChangedAt: time.Now(), - }) + state, err := agentsdk.ProtoFromLifecycleState(tt.state) if tt.wantErr { require.Error(t, err) return } + _, err = agentAPI.UpdateLifecycle(ctx, &agentproto.UpdateLifecycleRequest{ + Lifecycle: &agentproto.Lifecycle{ + State: state, + ChangedAt: timestamppb.Now(), + }, + }) require.NoError(t, err, "post lifecycle state %q", tt.state) workspace, err = client.Workspace(ctx, workspace.ID) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index f3a09c5357711..32222479b37ee 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -84,23 +84,6 @@ type PostMetadataRequest struct { // performance. type PostMetadataRequestDeprecated = codersdk.WorkspaceAgentMetadataResult -// PostMetadata posts agent metadata to the Coder server. -// -// Deprecated: use BatchUpdateMetadata on the agent dRPC API instead -func (c *Client) PostMetadata(ctx context.Context, req PostMetadataRequest) error { - res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/metadata", req) - if err != nil { - return xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusNoContent { - return codersdk.ReadBodyAsError(res) - } - - return nil -} - type Manifest struct { AgentID uuid.UUID `json:"agent_id"` AgentName string `json:"agent_name"` @@ -457,49 +440,11 @@ type StatsResponse struct { ReportInterval time.Duration `json:"report_interval"` } -// PostStats sends agent stats to the coder server -// -// Deprecated: uses agent API v1 endpoint -func (c *Client) PostStats(ctx context.Context, stats *Stats) (StatsResponse, error) { - res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-stats", stats) - if err != nil { - return StatsResponse{}, xerrors.Errorf("send request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return StatsResponse{}, codersdk.ReadBodyAsError(res) - } - - var interval StatsResponse - err = json.NewDecoder(res.Body).Decode(&interval) - if err != nil { - return StatsResponse{}, xerrors.Errorf("decode stats response: %w", err) - } - - return interval, nil -} - type PostLifecycleRequest struct { State codersdk.WorkspaceAgentLifecycle `json:"state"` ChangedAt time.Time `json:"changed_at"` } -// PostLifecycle posts the agent's lifecycle to the Coder server. -// -// Deprecated: Use UpdateLifecycle on the dRPC API instead -func (c *Client) PostLifecycle(ctx context.Context, req PostLifecycleRequest) error { - res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/report-lifecycle", req) - if err != nil { - return xerrors.Errorf("agent state post request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return codersdk.ReadBodyAsError(res) - } - - return nil -} - type PostStartupRequest struct { Version string `json:"version"` ExpandedDirectory string `json:"expanded_directory"` diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go index adfabd1510768..45ea9532c4c45 100644 --- a/codersdk/agentsdk/convert.go +++ b/codersdk/agentsdk/convert.go @@ -371,3 +371,11 @@ func LifecycleStateFromProto(s proto.Lifecycle_State) (codersdk.WorkspaceAgentLi } return codersdk.WorkspaceAgentLifecycle(strings.ToLower(caps)), nil } + +func ProtoFromLifecycleState(s codersdk.WorkspaceAgentLifecycle) (proto.Lifecycle_State, error) { + caps, ok := proto.Lifecycle_State_value[strings.ToUpper(string(s))] + if !ok { + return 0, xerrors.Errorf("unknown lifecycle state: %s", s) + } + return proto.Lifecycle_State(caps), nil +} diff --git a/docs/api/agents.md b/docs/api/agents.md index 13e5c38590d5c..e32fb0ac10f7a 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -160,67 +160,6 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Submit workspace agent application health - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/app-health \ - -H 'Content-Type: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /workspaceagents/me/app-health` - -> Body parameter - -```json -{ - "healths": { - "property1": "disabled", - "property2": "disabled" - } -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | -------------------------------------------------------------------------- | -------- | -------------------------- | -| `body` | body | [agentsdk.PostAppHealthsRequest](schemas.md#agentsdkpostapphealthsrequest) | true | Application health request | - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Coordinate workspace agent via Tailnet - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/coordinate \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /workspaceagents/me/coordinate` - -It accepts a WebSocket connection to an agent that listens to -incoming connections and publishes node updates. - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------------------------ | ------------------- | ------ | -| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get workspace agent external auth ### Code samples @@ -453,283 +392,6 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get authorized workspace agent manifest - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /workspaceagents/me/manifest` - -### Example responses - -> 200 Response - -```json -{ - "agent_id": "string", - "agent_name": "string", - "apps": [ - { - "command": "string", - "display_name": "string", - "external": true, - "health": "disabled", - "healthcheck": { - "interval": 0, - "threshold": 0, - "url": "string" - }, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "sharing_level": "owner", - "slug": "string", - "subdomain": true, - "subdomain_name": "string", - "url": "string" - } - ], - "derp_force_websockets": true, - "derpmap": { - "homeParams": { - "regionScore": { - "property1": 0, - "property2": 0 - } - }, - "omitDefaultRegions": true, - "regions": { - "property1": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "canPort80": true, - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - }, - "property2": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "canPort80": true, - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - } - } - }, - "directory": "string", - "disable_direct_connections": true, - "environment_variables": { - "property1": "string", - "property2": "string" - }, - "git_auth_configs": 0, - "metadata": [ - { - "display_name": "string", - "interval": 0, - "key": "string", - "script": "string", - "timeout": 0 - } - ], - "motd_file": "string", - "owner_name": "string", - "scripts": [ - { - "cron": "string", - "log_path": "string", - "log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a", - "run_on_start": true, - "run_on_stop": true, - "script": "string", - "start_blocks_login": true, - "timeout": 0 - } - ], - "vscode_port_proxy_uri": "string", - "workspace_id": "string", - "workspace_name": "string" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.Manifest](schemas.md#agentsdkmanifest) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Submit workspace agent stats - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /workspaceagents/me/report-stats` - -> Body parameter - -```json -{ - "connection_count": 0, - "connection_median_latency_ms": 0, - "connections_by_proto": { - "property1": 0, - "property2": 0 - }, - "metrics": [ - { - "labels": [ - { - "name": "string", - "value": "string" - } - ], - "name": "string", - "type": "counter", - "value": 0 - } - ], - "rx_bytes": 0, - "rx_packets": 0, - "session_count_jetbrains": 0, - "session_count_reconnecting_pty": 0, - "session_count_ssh": 0, - "session_count_vscode": 0, - "tx_bytes": 0, - "tx_packets": 0 -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | ------------------------------------------ | -------- | ------------- | -| `body` | body | [agentsdk.Stats](schemas.md#agentsdkstats) | true | Stats request | - -### Example responses - -> 200 Response - -```json -{ - "report_interval": 0 -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.StatsResponse](schemas.md#agentsdkstatsresponse) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Removed: Patch workspace agent logs - -### Code samples - -```shell -# Example request using curl -curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`PATCH /workspaceagents/me/startup-logs` - -> Body parameter - -```json -{ - "log_source_id": "string", - "logs": [ - { - "created_at": "string", - "level": "trace", - "output": "string" - } - ] -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | -------------------------------------------------- | -------- | ----------- | -| `body` | body | [agentsdk.PatchLogs](schemas.md#agentsdkpatchlogs) | true | logs | - -### Example responses - -> 200 Response - -```json -{ - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get workspace agent by ID ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 7770b091878bd..9a5f0d9e3c94a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -16,69 +16,6 @@ | `document` | string | true | | | | `signature` | string | true | | | -## agentsdk.AgentMetric - -```json -{ - "labels": [ - { - "name": "string", - "value": "string" - } - ], - "name": "string", - "type": "counter", - "value": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------- | --------------------------------------------------------------- | -------- | ------------ | ----------- | -| `labels` | array of [agentsdk.AgentMetricLabel](#agentsdkagentmetriclabel) | false | | | -| `name` | string | true | | | -| `type` | [agentsdk.AgentMetricType](#agentsdkagentmetrictype) | true | | | -| `value` | number | true | | | - -#### Enumerated Values - -| Property | Value | -| -------- | --------- | -| `type` | `counter` | -| `type` | `gauge` | - -## agentsdk.AgentMetricLabel - -```json -{ - "name": "string", - "value": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------- | ------ | -------- | ------------ | ----------- | -| `name` | string | true | | | -| `value` | string | true | | | - -## agentsdk.AgentMetricType - -```json -"counter" -``` - -### Properties - -#### Enumerated Values - -| Value | -| --------- | -| `counter` | -| `gauge` | - ## agentsdk.AuthenticateResponse ```json @@ -181,172 +118,6 @@ | `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | | | `output` | string | false | | | -## agentsdk.Manifest - -```json -{ - "agent_id": "string", - "agent_name": "string", - "apps": [ - { - "command": "string", - "display_name": "string", - "external": true, - "health": "disabled", - "healthcheck": { - "interval": 0, - "threshold": 0, - "url": "string" - }, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "sharing_level": "owner", - "slug": "string", - "subdomain": true, - "subdomain_name": "string", - "url": "string" - } - ], - "derp_force_websockets": true, - "derpmap": { - "homeParams": { - "regionScore": { - "property1": 0, - "property2": 0 - } - }, - "omitDefaultRegions": true, - "regions": { - "property1": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "canPort80": true, - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - }, - "property2": { - "avoid": true, - "embeddedRelay": true, - "nodes": [ - { - "canPort80": true, - "certName": "string", - "derpport": 0, - "forceHTTP": true, - "hostName": "string", - "insecureForTests": true, - "ipv4": "string", - "ipv6": "string", - "name": "string", - "regionID": 0, - "stunonly": true, - "stunport": 0, - "stuntestIP": "string" - } - ], - "regionCode": "string", - "regionID": 0, - "regionName": "string" - } - } - }, - "directory": "string", - "disable_direct_connections": true, - "environment_variables": { - "property1": "string", - "property2": "string" - }, - "git_auth_configs": 0, - "metadata": [ - { - "display_name": "string", - "interval": 0, - "key": "string", - "script": "string", - "timeout": 0 - } - ], - "motd_file": "string", - "owner_name": "string", - "scripts": [ - { - "cron": "string", - "log_path": "string", - "log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a", - "run_on_start": true, - "run_on_stop": true, - "script": "string", - "start_blocks_login": true, - "timeout": 0 - } - ], - "vscode_port_proxy_uri": "string", - "workspace_id": "string", - "workspace_name": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `agent_id` | string | false | | | -| `agent_name` | string | false | | | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `derp_force_websockets` | boolean | false | | | -| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | -| `directory` | string | false | | | -| `disable_direct_connections` | boolean | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. | -| `metadata` | array of [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | | -| `motd_file` | string | false | | | -| `owner_name` | string | false | | Owner name and WorkspaceID are used by an open-source user to identify the workspace. We do not provide insurance that this will not be removed in the future, but if it's easy to persist lets keep it around. | -| `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | | -| `vscode_port_proxy_uri` | string | false | | | -| `workspace_id` | string | false | | | -| `workspace_name` | string | false | | | - -## agentsdk.Metadata - -```json -{ - "age": 0, - "collected_at": "2019-08-24T14:15:22Z", - "error": "string", - "key": "string", - "value": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| `age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `collected_at` | string | false | | | -| `error` | string | false | | | -| `key` | string | false | | | -| `value` | string | false | | | - ## agentsdk.PatchLogs ```json @@ -369,40 +140,6 @@ | `log_source_id` | string | false | | | | `logs` | array of [agentsdk.Log](#agentsdklog) | false | | | -## agentsdk.PostAppHealthsRequest - -```json -{ - "healths": { - "property1": "disabled", - "property2": "disabled" - } -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | ---------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------- | -| `healths` | object | false | | Healths is a map of the workspace app name and the health of the app. | -| » `[any property]` | [codersdk.WorkspaceAppHealth](#codersdkworkspaceapphealth) | false | | | - -## agentsdk.PostLifecycleRequest - -```json -{ - "changed_at": "string", - "state": "created" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------ | -------------------------------------------------------------------- | -------- | ------------ | ----------- | -| `changed_at` | string | false | | | -| `state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | - ## agentsdk.PostLogSourceRequest ```json @@ -421,132 +158,6 @@ | `icon` | string | false | | | | `id` | string | false | | ID is a unique identifier for the log source. It is scoped to a workspace agent, and can be statically defined inside code to prevent duplicate sources from being created for the same agent. | -## agentsdk.PostMetadataRequest - -```json -{ - "metadata": [ - { - "age": 0, - "collected_at": "2019-08-24T14:15:22Z", - "error": "string", - "key": "string", - "value": "string" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------- | ----------------------------------------------- | -------- | ------------ | ----------- | -| `metadata` | array of [agentsdk.Metadata](#agentsdkmetadata) | false | | | - -## agentsdk.PostMetadataRequestDeprecated - -```json -{ - "age": 0, - "collected_at": "2019-08-24T14:15:22Z", - "error": "string", - "value": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| `age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `collected_at` | string | false | | | -| `error` | string | false | | | -| `value` | string | false | | | - -## agentsdk.PostStartupRequest - -```json -{ - "expanded_directory": "string", - "subsystems": ["envbox"], - "version": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------------- | ----------------------------------------------------------- | -------- | ------------ | ----------- | -| `expanded_directory` | string | false | | | -| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | -| `version` | string | false | | | - -## agentsdk.Stats - -```json -{ - "connection_count": 0, - "connection_median_latency_ms": 0, - "connections_by_proto": { - "property1": 0, - "property2": 0 - }, - "metrics": [ - { - "labels": [ - { - "name": "string", - "value": "string" - } - ], - "name": "string", - "type": "counter", - "value": 0 - } - ], - "rx_bytes": 0, - "rx_packets": 0, - "session_count_jetbrains": 0, - "session_count_reconnecting_pty": 0, - "session_count_ssh": 0, - "session_count_vscode": 0, - "tx_bytes": 0, - "tx_packets": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------------------------- | ----------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | -| `connection_count` | integer | false | | Connection count is the number of connections received by an agent. | -| `connection_median_latency_ms` | number | false | | Connection median latency ms is the median latency of all connections in milliseconds. | -| `connections_by_proto` | object | false | | Connections by proto is a count of connections by protocol. | -| » `[any property]` | integer | false | | | -| `metrics` | array of [agentsdk.AgentMetric](#agentsdkagentmetric) | false | | Metrics collected by the agent | -| `rx_bytes` | integer | false | | Rx bytes is the number of received bytes. | -| `rx_packets` | integer | false | | Rx packets is the number of received packets. | -| `session_count_jetbrains` | integer | false | | Session count jetbrains is the number of connections received by an agent that are from our JetBrains extension. | -| `session_count_reconnecting_pty` | integer | false | | Session count reconnecting pty is the number of connections received by an agent that are from the reconnecting web terminal. | -| `session_count_ssh` | integer | false | | Session count ssh is the number of connections received by an agent that are normal, non-tagged SSH sessions. | -| `session_count_vscode` | integer | false | | Session count vscode is the number of connections received by an agent that are from our VS Code extension. | -| `tx_bytes` | integer | false | | Tx bytes is the number of transmitted bytes. | -| `tx_packets` | integer | false | | Tx packets is the number of transmitted bytes. | - -## agentsdk.StatsResponse - -```json -{ - "report_interval": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ----------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------ | -| `report_interval` | integer | false | | Report interval is the duration after which the agent should send stats again. | - ## coderd.SCIMUser ```json @@ -6408,28 +6019,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `id` | string | false | | | | `workspace_agent_id` | string | false | | | -## codersdk.WorkspaceAgentMetadataDescription - -```json -{ - "display_name": "string", - "interval": 0, - "key": "string", - "script": "string", - "timeout": 0 -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | ----------- | -| `display_name` | string | false | | | -| `interval` | integer | false | | | -| `key` | string | false | | | -| `script` | string | false | | | -| `timeout` | integer | false | | | - ## codersdk.WorkspaceAgentPortShare ```json From b3412661e68223a4a8b85d3f70c7ab775c4be14a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 06:12:24 +0000 Subject: [PATCH 4/8] ported workspaceagent test --- coderd/workspaceagentsrpc_test.go | 122 ++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/coderd/workspaceagentsrpc_test.go b/coderd/workspaceagentsrpc_test.go index a92fbdcd1ca1a..141211c1236fd 100644 --- a/coderd/workspaceagentsrpc_test.go +++ b/coderd/workspaceagentsrpc_test.go @@ -1,19 +1,141 @@ package coderd_test import ( + "context" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) +// Ported to RPC API from coderd/workspaceagents_test.go +func TestWorkspaceAgentReportStats(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + ac := agentsdk.New(client.URL) + ac.SetSessionToken(r.AgentToken) + conn, err := ac.ConnectRPC(context.Background()) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + agentAPI := agentproto.NewDRPCAgentClient(conn) + + _, err = agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{"TCP": 1}, + ConnectionCount: 1, + RxPackets: 1, + RxBytes: 1, + TxPackets: 1, + TxBytes: 1, + SessionCountVscode: 1, + SessionCountJetbrains: 0, + SessionCountReconnectingPty: 0, + SessionCountSsh: 0, + ConnectionMedianLatencyMs: 10, + }, + }) + require.NoError(t, err) + + newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) + require.NoError(t, err) + + assert.True(t, + newWorkspace.LastUsedAt.After(r.Workspace.LastUsedAt), + "%s is not after %s", newWorkspace.LastUsedAt, r.Workspace.LastUsedAt, + ) + }) + + t.Run("FailDeleted", func(t *testing.T) { + t.Parallel() + + owner, db := coderdtest.NewWithDatabase(t, nil) + ownerUser := coderdtest.CreateFirstUser(t, owner) + client, admin := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: admin.OrganizationIDs[0], + OwnerID: admin.ID, + }).WithAgent().Do() + + ac := agentsdk.New(client.URL) + ac.SetSessionToken(r.AgentToken) + conn, err := ac.ConnectRPC(context.Background()) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + agentAPI := agentproto.NewDRPCAgentClient(conn) + + _, err = agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{"TCP": 1}, + ConnectionCount: 1, + RxPackets: 1, + RxBytes: 1, + TxPackets: 1, + TxBytes: 1, + SessionCountVscode: 0, + SessionCountJetbrains: 0, + SessionCountReconnectingPty: 0, + SessionCountSsh: 0, + ConnectionMedianLatencyMs: 10, + }, + }) + require.NoError(t, err) + + newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) + require.NoError(t, err) + + // nolint:gocritic // using db directly over creating a delete job + err = db.UpdateWorkspaceDeletedByID(dbauthz.As(context.Background(), + coderdtest.AuthzUserSubject(admin, ownerUser.OrganizationID)), + database.UpdateWorkspaceDeletedByIDParams{ + ID: newWorkspace.ID, + Deleted: true, + }) + require.NoError(t, err) + + resp, err := agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{"TCP": 1}, + ConnectionCount: 1, + RxPackets: 1, + RxBytes: 1, + TxPackets: 1, + TxBytes: 1, + SessionCountVscode: 1, + SessionCountJetbrains: 0, + SessionCountReconnectingPty: 0, + SessionCountSsh: 0, + ConnectionMedianLatencyMs: 10, + }, + }) + require.Nil(t, resp) + require.ErrorContains(t, err, "agent is invalid") + }) +} + func TestAgentAPI_LargeManifest(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) From 4594119ff50abaf54daad1fcaa88ceaffd8bb5a1 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 06:54:38 +0000 Subject: [PATCH 5/8] removed useless test --- coderd/workspaceagentsrpc_test.go | 144 ++++++++---------------------- 1 file changed, 35 insertions(+), 109 deletions(-) diff --git a/coderd/workspaceagentsrpc_test.go b/coderd/workspaceagentsrpc_test.go index 141211c1236fd..ca8f334d4e766 100644 --- a/coderd/workspaceagentsrpc_test.go +++ b/coderd/workspaceagentsrpc_test.go @@ -10,9 +10,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" - "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" @@ -22,118 +20,46 @@ import ( func TestWorkspaceAgentReportStats(t *testing.T) { t.Parallel() - t.Run("OK", func(t *testing.T) { - t.Parallel() + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() - client, db := coderdtest.NewWithDatabase(t, nil) - user := coderdtest.CreateFirstUser(t, client) - r := dbfake.WorkspaceBuild(t, db, database.Workspace{ - OrganizationID: user.OrganizationID, - OwnerID: user.UserID, - }).WithAgent().Do() - - ac := agentsdk.New(client.URL) - ac.SetSessionToken(r.AgentToken) - conn, err := ac.ConnectRPC(context.Background()) - require.NoError(t, err) - defer func() { - _ = conn.Close() - }() - agentAPI := agentproto.NewDRPCAgentClient(conn) - - _, err = agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ - Stats: &agentproto.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVscode: 1, - SessionCountJetbrains: 0, - SessionCountReconnectingPty: 0, - SessionCountSsh: 0, - ConnectionMedianLatencyMs: 10, - }, - }) - require.NoError(t, err) - - newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) - require.NoError(t, err) + ac := agentsdk.New(client.URL) + ac.SetSessionToken(r.AgentToken) + conn, err := ac.ConnectRPC(context.Background()) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + agentAPI := agentproto.NewDRPCAgentClient(conn) - assert.True(t, - newWorkspace.LastUsedAt.After(r.Workspace.LastUsedAt), - "%s is not after %s", newWorkspace.LastUsedAt, r.Workspace.LastUsedAt, - ) + _, err = agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{"TCP": 1}, + ConnectionCount: 1, + RxPackets: 1, + RxBytes: 1, + TxPackets: 1, + TxBytes: 1, + SessionCountVscode: 1, + SessionCountJetbrains: 0, + SessionCountReconnectingPty: 0, + SessionCountSsh: 0, + ConnectionMedianLatencyMs: 10, + }, }) + require.NoError(t, err) - t.Run("FailDeleted", func(t *testing.T) { - t.Parallel() - - owner, db := coderdtest.NewWithDatabase(t, nil) - ownerUser := coderdtest.CreateFirstUser(t, owner) - client, admin := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()) - r := dbfake.WorkspaceBuild(t, db, database.Workspace{ - OrganizationID: admin.OrganizationIDs[0], - OwnerID: admin.ID, - }).WithAgent().Do() - - ac := agentsdk.New(client.URL) - ac.SetSessionToken(r.AgentToken) - conn, err := ac.ConnectRPC(context.Background()) - require.NoError(t, err) - defer func() { - _ = conn.Close() - }() - agentAPI := agentproto.NewDRPCAgentClient(conn) - - _, err = agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ - Stats: &agentproto.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVscode: 0, - SessionCountJetbrains: 0, - SessionCountReconnectingPty: 0, - SessionCountSsh: 0, - ConnectionMedianLatencyMs: 10, - }, - }) - require.NoError(t, err) - - newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) - require.NoError(t, err) - - // nolint:gocritic // using db directly over creating a delete job - err = db.UpdateWorkspaceDeletedByID(dbauthz.As(context.Background(), - coderdtest.AuthzUserSubject(admin, ownerUser.OrganizationID)), - database.UpdateWorkspaceDeletedByIDParams{ - ID: newWorkspace.ID, - Deleted: true, - }) - require.NoError(t, err) + newWorkspace, err := client.Workspace(context.Background(), r.Workspace.ID) + require.NoError(t, err) - resp, err := agentAPI.UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ - Stats: &agentproto.Stats{ - ConnectionsByProto: map[string]int64{"TCP": 1}, - ConnectionCount: 1, - RxPackets: 1, - RxBytes: 1, - TxPackets: 1, - TxBytes: 1, - SessionCountVscode: 1, - SessionCountJetbrains: 0, - SessionCountReconnectingPty: 0, - SessionCountSsh: 0, - ConnectionMedianLatencyMs: 10, - }, - }) - require.Nil(t, resp) - require.ErrorContains(t, err, "agent is invalid") - }) + assert.True(t, + newWorkspace.LastUsedAt.After(r.Workspace.LastUsedAt), + "%s is not after %s", newWorkspace.LastUsedAt, r.Workspace.LastUsedAt, + ) } func TestAgentAPI_LargeManifest(t *testing.T) { From 167d4ebe913cade7579bf2874dc0ab6db9ae9459 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 14:28:16 +0000 Subject: [PATCH 6/8] bump outdatedAgent version --- site/e2e/tests/outdatedAgent.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts index c6bccba658be4..48393c63f7d0e 100644 --- a/site/e2e/tests/outdatedAgent.spec.ts +++ b/site/e2e/tests/outdatedAgent.spec.ts @@ -11,8 +11,8 @@ import { } from "../helpers"; import { beforeCoderTest } from "../hooks"; -// we no longer support versions prior to single tailnet: https://github.com/coder/coder/commit/d7cbdbd9c64ad26821e6b35834c59ecf85dcd9d4 -const agentVersion = "v0.27.0"; +// we no longer support versions w/o DRPC +const agentVersion = "v2.12.1"; test.beforeEach(({ page }) => beforeCoderTest(page)); From dae11a4d7786453718d0bca1ec6e839bac3bd34f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 6 Jun 2024 15:18:21 +0000 Subject: [PATCH 7/8] port test --- .../insights/metricscollector_test.go | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 coderd/prometheusmetrics/insights/metricscollector_test.go diff --git a/coderd/prometheusmetrics/insights/metricscollector_test.go b/coderd/prometheusmetrics/insights/metricscollector_test.go new file mode 100644 index 0000000000000..c33c11117bca6 --- /dev/null +++ b/coderd/prometheusmetrics/insights/metricscollector_test.go @@ -0,0 +1,217 @@ +package insights_test + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + io_prometheus_client "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/prometheusmetrics/insights" + "github.com/coder/coder/v2/coderd/workspaceapps" + "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestCollectInsights(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + + options := &coderdtest.Options{ + IncludeProvisionerDaemon: true, + AgentStatsRefreshInterval: time.Millisecond * 100, + Database: db, + Pubsub: ps, + } + ownerClient := coderdtest.New(t, options) + ownerClient.SetLogger(logger.Named("ownerClient").Leveled(slog.LevelDebug)) + owner := coderdtest.CreateFirstUser(t, ownerClient) + client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) + + // Given + // Initialize metrics collector + mc, err := insights.NewMetricsCollector(db, logger, 0, time.Second) + require.NoError(t, err) + + registry := prometheus.NewRegistry() + registry.Register(mc) + + var ( + orgID = owner.OrganizationID + tpl = dbgen.Template(t, db, database.Template{OrganizationID: orgID, CreatedBy: user.ID, Name: "golden-template"}) + ver = dbgen.TemplateVersion(t, db, database.TemplateVersion{OrganizationID: orgID, CreatedBy: user.ID, TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}}) + param1 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "first_parameter"}) + param2 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "second_parameter", Type: "bool"}) + param3 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "third_parameter", Type: "number"}) + workspace1 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID}) + workspace2 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID}) + job1 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID}) + job2 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID}) + build1 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace1.ID, JobID: job1.ID}) + build2 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace2.ID, JobID: job2.ID}) + res1 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build1.JobID}) + res2 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build2.JobID}) + agent1 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res1.ID}) + agent2 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res2.ID}) + app1 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent1.ID, Slug: "golden-slug", DisplayName: "Golden Slug"}) + app2 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent2.ID, Slug: "golden-slug", DisplayName: "Golden Slug"}) + _ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ + {WorkspaceBuildID: build1.ID, Name: param1.Name, Value: "Foobar"}, + {WorkspaceBuildID: build1.ID, Name: param2.Name, Value: "true"}, + {WorkspaceBuildID: build1.ID, Name: param3.Name, Value: "789"}, + }) + _ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ + {WorkspaceBuildID: build2.ID, Name: param1.Name, Value: "Baz"}, + {WorkspaceBuildID: build2.ID, Name: param2.Name, Value: "true"}, + {WorkspaceBuildID: build2.ID, Name: param3.Name, Value: "999"}, + }) + ) + + // Start an agent so that we can generate stats. + var agentClients []agentproto.DRPCAgentClient + for i, agent := range []database.WorkspaceAgent{agent1, agent2} { + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(agent.AuthToken.String()) + agentClient.SDK.SetLogger(logger.Leveled(slog.LevelDebug).Named(fmt.Sprintf("agent%d", i+1))) + conn, err := agentClient.ConnectRPC(context.Background()) + require.NoError(t, err) + agentAPI := agentproto.NewDRPCAgentClient(conn) + agentClients = append(agentClients, agentAPI) + } + + // Fake app stats + _, err = agentClients[0].UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + // ConnectionCount must be positive as database query ignores stats with no active connections at the time frame + ConnectionsByProto: map[string]int64{"TCP": 1}, + ConnectionCount: 1, + ConnectionMedianLatencyMs: 15, + // Session counts must be positive, but the exact value is ignored. + // Database query approximates it to 60s of usage. + SessionCountSsh: 99, + SessionCountJetbrains: 47, + SessionCountVscode: 34, + }, + }) + require.NoError(t, err, "unable to post fake stats") + + // Fake app usage + reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ + Database: db, + AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + }) + refTime := time.Now().Add(-3 * time.Minute).Truncate(time.Minute) + //nolint:gocritic // This is a test. + err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(context.Background()), []workspaceapps.StatsReport{ + { + UserID: user.ID, + WorkspaceID: workspace1.ID, + AgentID: agent1.ID, + AccessMethod: "path", + SlugOrPort: app1.Slug, + SessionID: uuid.New(), + SessionStartedAt: refTime, + SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second), + Requests: 1, + }, + // Same usage on differrent workspace/agent in same template, + // should not be counted as extra. + { + UserID: user.ID, + WorkspaceID: workspace2.ID, + AgentID: agent2.ID, + AccessMethod: "path", + SlugOrPort: app2.Slug, + SessionID: uuid.New(), + SessionStartedAt: refTime, + SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second), + Requests: 1, + }, + { + UserID: user.ID, + WorkspaceID: workspace2.ID, + AgentID: agent2.ID, + AccessMethod: "path", + SlugOrPort: app2.Slug, + SessionID: uuid.New(), + SessionStartedAt: refTime.Add(2 * time.Minute), + SessionEndedAt: refTime.Add(2 * time.Minute).Add(30 * time.Second), + Requests: 1, + }, + }) + require.NoError(t, err, "want no error inserting app stats") + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Run metrics collector + closeFunc, err := mc.Run(ctx) + require.NoError(t, err) + defer closeFunc() + + goldenFile, err := os.ReadFile("testdata/insights-metrics.json") + require.NoError(t, err) + golden := map[string]int{} + err = json.Unmarshal(goldenFile, &golden) + require.NoError(t, err) + + collected := map[string]int{} + ok := assert.Eventuallyf(t, func() bool { + // When + metrics, err := registry.Gather() + if !assert.NoError(t, err) { + return false + } + + // Then + for _, metric := range metrics { + t.Logf("metric: %s: %#v", metric.GetName(), metric) + switch metric.GetName() { + case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users", "coderd_insights_parameters": + for _, m := range metric.Metric { + key := metric.GetName() + if len(m.Label) > 0 { + key = key + "[" + metricLabelAsString(m) + "]" + } + collected[key] = int(m.Gauge.GetValue()) + } + default: + assert.Failf(t, "unexpected metric collected", "metric: %s", metric.GetName()) + } + } + + return assert.ObjectsAreEqualValues(golden, collected) + }, testutil.WaitMedium, testutil.IntervalFast, "template insights are inconsistent with golden files") + if !ok { + diff := cmp.Diff(golden, collected) + assert.Empty(t, diff, "template insights are inconsistent with golden files (-golden +collected)") + } +} + +func metricLabelAsString(m *io_prometheus_client.Metric) string { + var labels []string + for _, labelPair := range m.Label { + labels = append(labels, labelPair.GetName()+"="+labelPair.GetValue()) + } + return strings.Join(labels, ",") +} From fd3628f5c31138534fe9806d1e7fbcfd6eedaeb6 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 7 Jun 2024 03:00:55 +0000 Subject: [PATCH 8/8] close conns --- coderd/prometheusmetrics/insights/metricscollector_test.go | 7 +++++++ coderd/workspaceagents_test.go | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/coderd/prometheusmetrics/insights/metricscollector_test.go b/coderd/prometheusmetrics/insights/metricscollector_test.go index c33c11117bca6..9179c9896235d 100644 --- a/coderd/prometheusmetrics/insights/metricscollector_test.go +++ b/coderd/prometheusmetrics/insights/metricscollector_test.go @@ -99,6 +99,13 @@ func TestCollectInsights(t *testing.T) { agentClients = append(agentClients, agentAPI) } + defer func() { + for a := range agentClients { + err := agentClients[a].DRPCConn().Close() + require.NoError(t, err) + } + }() + // Fake app stats _, err = agentClients[0].UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{ Stats: &agentproto.Stats{ diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index aed4e63349040..f50a886205cdf 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -988,6 +988,10 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { ac.SetSessionToken(r.AgentToken) conn, err := ac.ConnectRPC(ctx) require.NoError(t, err) + defer func() { + cErr := conn.Close() + require.NoError(t, cErr) + }() agentAPI := agentproto.NewDRPCAgentClient(conn) tests := []struct {