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 { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy