mirror of https://github.com/coder/coder.git
feat: Add anonymized telemetry to report product usage (#2273)
* feat: Add anonymized telemetry to report product usage This adds a background service to report telemetry to a Coder server for usage data. There will be realtime event data sent in the future, but for now usage will report on a CRON. * Fix flake and requested changes * Add reporting options for setup * Add reporting for workspaces * Add resources as they are reported * Track API key usage * Ensure telemetry is tracked prior to exit
This commit is contained in:
parent
af8a1e3fea
commit
4cce969018
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/coreos/go-systemd/daemon"
|
||||
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
||||
"github.com/google/go-github/v43/github"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/turn/v2"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
@ -53,6 +54,7 @@ import (
|
|||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/devtunnel"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/coderd/turnconn"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
@ -81,6 +83,7 @@ func server() *cobra.Command {
|
|||
oauth2GithubClientSecret string
|
||||
oauth2GithubAllowedOrganizations []string
|
||||
oauth2GithubAllowSignups bool
|
||||
telemetryURL string
|
||||
tlsCertFile string
|
||||
tlsClientCAFile string
|
||||
tlsClientAuth string
|
||||
|
@ -134,6 +137,7 @@ func server() *cobra.Command {
|
|||
}
|
||||
|
||||
config := createConfig(cmd)
|
||||
builtinPostgres := false
|
||||
// Only use built-in if PostgreSQL URL isn't specified!
|
||||
if !inMemoryDatabase && postgresURL == "" {
|
||||
var closeFunc func() error
|
||||
|
@ -142,6 +146,7 @@ func server() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
builtinPostgres = true
|
||||
defer func() {
|
||||
// Gracefully shut PostgreSQL down!
|
||||
_ = closeFunc()
|
||||
|
@ -253,6 +258,7 @@ func server() *cobra.Command {
|
|||
SSHKeygenAlgorithm: sshKeygenAlgorithm,
|
||||
TURNServer: turnServer,
|
||||
TracerProvider: tracerProvider,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
}
|
||||
|
||||
if oauth2GithubClientSecret != "" {
|
||||
|
@ -285,6 +291,44 @@ func server() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
deploymentID, err := options.Database.GetDeploymentID(cmd.Context())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get deployment id: %w", err)
|
||||
}
|
||||
if deploymentID == "" {
|
||||
deploymentID = uuid.NewString()
|
||||
err = options.Database.InsertDeploymentID(cmd.Context(), deploymentID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set deployment id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the raw telemetry URL!
|
||||
telemetryURL, err := url.Parse(telemetryURL)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse telemetry url: %w", err)
|
||||
}
|
||||
if !inMemoryDatabase || cmd.Flags().Changed("telemetry-url") {
|
||||
options.Telemetry, err = telemetry.New(telemetry.Options{
|
||||
BuiltinPostgres: builtinPostgres,
|
||||
DeploymentID: deploymentID,
|
||||
Database: options.Database,
|
||||
Logger: logger.Named("telemetry"),
|
||||
URL: telemetryURL,
|
||||
GitHubOAuth: oauth2GithubClientID != "",
|
||||
Prometheus: promEnabled,
|
||||
STUN: len(stunServers) != 0,
|
||||
Tunnel: tunnel,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create telemetry reporter: %w", err)
|
||||
}
|
||||
defer options.Telemetry.Close()
|
||||
}
|
||||
|
||||
coderAPI := coderd.New(options)
|
||||
client := codersdk.New(localURL)
|
||||
if tlsEnable {
|
||||
|
@ -438,6 +482,8 @@ func server() *cobra.Command {
|
|||
<-devTunnelErrChan
|
||||
}
|
||||
|
||||
// Ensures a last report can be sent before exit!
|
||||
options.Telemetry.Close()
|
||||
cmd.Println("Waiting for WebSocket connections to close...")
|
||||
shutdownConns()
|
||||
coderAPI.Close()
|
||||
|
@ -485,6 +531,8 @@ func server() *cobra.Command {
|
|||
"Specifies organizations the user must be a member of to authenticate with GitHub.")
|
||||
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
|
||||
"Specifies whether new users can sign up with GitHub.")
|
||||
cliflag.StringVarP(root.Flags(), &telemetryURL, "telemetry-url", "", "CODER_TELEMETRY_URL", "https://telemetry.coder.com", "Specifies a URL to send telemetry to.")
|
||||
_ = root.Flags().MarkHidden("telemetry-url")
|
||||
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
|
||||
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
|
||||
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+
|
||||
|
|
|
@ -8,10 +8,12 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
|
@ -19,12 +21,14 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
|
@ -233,6 +237,37 @@ func TestServer(t *testing.T) {
|
|||
require.ErrorIs(t, <-errC, context.Canceled)
|
||||
require.Error(t, goleak.Find())
|
||||
})
|
||||
t.Run("Telemetry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
deployment := make(chan struct{}, 64)
|
||||
snapshot := make(chan *telemetry.Snapshot, 64)
|
||||
r := chi.NewRouter()
|
||||
r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
deployment <- struct{}{}
|
||||
})
|
||||
r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
ss := &telemetry.Snapshot{}
|
||||
err := json.NewDecoder(r.Body).Decode(ss)
|
||||
require.NoError(t, err)
|
||||
snapshot <- ss
|
||||
})
|
||||
server := httptest.NewServer(r)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--telemetry-url", server.URL)
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- root.ExecuteContext(ctx)
|
||||
}()
|
||||
|
||||
<-deployment
|
||||
<-snapshot
|
||||
})
|
||||
}
|
||||
|
||||
func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/coderd/turnconn"
|
||||
"github.com/coder/coder/coderd/wsconncache"
|
||||
|
@ -54,6 +55,7 @@ type Options struct {
|
|||
ICEServers []webrtc.ICEServer
|
||||
SecureAuthCookie bool
|
||||
SSHKeygenAlgorithm gitsshkey.Algorithm
|
||||
Telemetry telemetry.Reporter
|
||||
TURNServer *turnconn.Server
|
||||
TracerProvider *sdktrace.TracerProvider
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
|
@ -166,6 +167,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API)
|
|||
TURNServer: turnServer,
|
||||
APIRateLimit: options.APIRateLimit,
|
||||
Authorizer: options.Authorizer,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
})
|
||||
srv.Config.Handler = coderAPI.Handler
|
||||
if options.IncludeProvisionerD {
|
||||
|
|
|
@ -66,6 +66,8 @@ type fakeQuerier struct {
|
|||
workspaceBuilds []database.WorkspaceBuild
|
||||
workspaceApps []database.WorkspaceApp
|
||||
workspaces []database.Workspace
|
||||
|
||||
deploymentID string
|
||||
}
|
||||
|
||||
// InTx doesn't rollback data properly for in-memory yet.
|
||||
|
@ -128,6 +130,19 @@ func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIK
|
|||
return database.APIKey{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time) ([]database.APIKey, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
apiKeys := make([]database.APIKey, 0)
|
||||
for _, key := range q.apiKeys {
|
||||
if key.LastUsed.After(after) {
|
||||
apiKeys = append(apiKeys, key)
|
||||
}
|
||||
}
|
||||
return apiKeys, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
@ -315,7 +330,7 @@ func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.U
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesWithFilter(_ context.Context, arg database.GetWorkspacesWithFilterParams) ([]database.Workspace, error) {
|
||||
func (q *fakeQuerier) GetWorkspaces(_ context.Context, arg database.GetWorkspacesParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
|
@ -421,6 +436,19 @@ func (q *fakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID)
|
|||
return apps, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAppsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceApp, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
apps := make([]database.WorkspaceApp, 0)
|
||||
for _, app := range q.workspaceApps {
|
||||
if app.CreatedAt.After(after) {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -647,6 +675,19 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con
|
|||
return database.WorkspaceBuild{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceBuilds := make([]database.WorkspaceBuild, 0)
|
||||
for _, workspaceBuild := range q.workspaceBuilds {
|
||||
if workspaceBuild.CreatedAt.After(after) {
|
||||
workspaceBuilds = append(workspaceBuilds, workspaceBuild)
|
||||
}
|
||||
}
|
||||
return workspaceBuilds, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -870,6 +911,19 @@ func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg dat
|
|||
return version, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetTemplateVersionsCreatedAfter(_ context.Context, after time.Time) ([]database.TemplateVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
versions := make([]database.TemplateVersion, 0)
|
||||
for _, version := range q.templateVersions {
|
||||
if version.CreatedAt.After(after) {
|
||||
versions = append(versions, version)
|
||||
}
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, arg database.GetTemplateVersionByTemplateIDAndNameParams) (database.TemplateVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -929,6 +983,19 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
|
|||
return parameters, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetParameterSchemasCreatedAfter(_ context.Context, after time.Time) ([]database.ParameterSchema, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
parameters := make([]database.ParameterSchema, 0)
|
||||
for _, parameterSchema := range q.parameterSchemas {
|
||||
if parameterSchema.CreatedAt.After(after) {
|
||||
parameters = append(parameters, parameterSchema)
|
||||
}
|
||||
}
|
||||
return parameters, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -948,6 +1015,13 @@ func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg dat
|
|||
return database.ParameterValue{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetTemplates(_ context.Context) ([]database.Template, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
return q.templates[:], nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -1095,6 +1169,19 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc
|
|||
return workspaceAgents, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceAgent, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceAgents := make([]database.WorkspaceAgent, 0)
|
||||
for _, agent := range q.provisionerJobAgents {
|
||||
if agent.CreatedAt.After(after) {
|
||||
workspaceAgents = append(workspaceAgents, agent)
|
||||
}
|
||||
}
|
||||
return workspaceAgents, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndName(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndNameParams) (database.WorkspaceApp, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -1166,6 +1253,19 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid
|
|||
return resources, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceResource, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
resources := make([]database.WorkspaceResource, 0)
|
||||
for _, resource := range q.provisionerJobResources {
|
||||
if resource.CreatedAt.After(after) {
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -1186,6 +1286,19 @@ func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID
|
|||
return jobs, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after time.Time) ([]database.ProvisionerJob, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
jobs := make([]database.ProvisionerJob, 0)
|
||||
for _, job := range q.provisionerJobs {
|
||||
if job.CreatedAt.After(after) {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerLogsByIDBetween(_ context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -1966,3 +2079,18 @@ func (q *fakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit
|
|||
|
||||
return alog, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) InsertDeploymentID(_ context.Context, id string) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
q.deploymentID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetDeploymentID(_ context.Context) (string, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
return q.deploymentID, nil
|
||||
}
|
||||
|
|
|
@ -226,6 +226,11 @@ CREATE TABLE provisioner_jobs (
|
|||
worker_id uuid
|
||||
);
|
||||
|
||||
CREATE TABLE site_configs (
|
||||
key character varying(256) NOT NULL,
|
||||
value character varying(8192) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE template_versions (
|
||||
id uuid NOT NULL,
|
||||
template_id uuid,
|
||||
|
@ -378,6 +383,9 @@ ALTER TABLE ONLY provisioner_job_logs
|
|||
ALTER TABLE ONLY provisioner_jobs
|
||||
ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY site_configs
|
||||
ADD CONSTRAINT site_configs_key_key UNIQUE (key);
|
||||
|
||||
ALTER TABLE ONLY template_versions
|
||||
ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE site_configs;
|
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE IF NOT EXISTS site_configs (
|
||||
key varchar(256) NOT NULL UNIQUE,
|
||||
value varchar(8192) NOT NULL
|
||||
);
|
|
@ -426,6 +426,11 @@ type ProvisionerJobLog struct {
|
|||
Output string `db:"output" json:"output"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Key string `db:"key" json:"key"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
|
|
|
@ -6,6 +6,7 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -22,12 +23,14 @@ type querier interface {
|
|||
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
|
||||
// GetAuditLogsBefore retrieves `limit` number of audit logs before the provided
|
||||
// ID.
|
||||
GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error)
|
||||
// This function returns roles for authorization purposes. Implied member roles
|
||||
// are included.
|
||||
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
|
||||
GetDeploymentID(ctx context.Context) (string, error)
|
||||
GetFileByHash(ctx context.Context, hash string) (File, error)
|
||||
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
|
||||
GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
|
||||
|
@ -40,12 +43,14 @@ type querier interface {
|
|||
GetOrganizations(ctx context.Context) ([]Organization, error)
|
||||
GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error)
|
||||
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
|
||||
GetParameterSchemasCreatedAfter(ctx context.Context, createdAt time.Time) ([]ParameterSchema, error)
|
||||
GetParameterValueByScopeAndName(ctx context.Context, arg GetParameterValueByScopeAndNameParams) (ParameterValue, error)
|
||||
GetParameterValuesByScope(ctx context.Context, arg GetParameterValuesByScopeParams) ([]ParameterValue, error)
|
||||
GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error)
|
||||
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
|
||||
GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error)
|
||||
GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error)
|
||||
GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error)
|
||||
GetProvisionerLogsByIDBetween(ctx context.Context, arg GetProvisionerLogsByIDBetweenParams) ([]ProvisionerJobLog, error)
|
||||
GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error)
|
||||
GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error)
|
||||
|
@ -53,6 +58,8 @@ type querier interface {
|
|||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
|
||||
GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error)
|
||||
GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error)
|
||||
GetTemplates(ctx context.Context) ([]Template, error)
|
||||
GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error)
|
||||
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
||||
|
@ -63,23 +70,28 @@ type querier interface {
|
|||
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
|
||||
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg GetWorkspaceAppByAgentIDAndNameParams) (WorkspaceApp, error)
|
||||
GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error)
|
||||
GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error)
|
||||
GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error)
|
||||
GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDParams) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
|
||||
GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByTemplateIDsRow, error)
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
|
||||
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error)
|
||||
GetWorkspacesAutostart(ctx context.Context) ([]Workspace, error)
|
||||
GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
|
||||
InsertDeploymentID(ctx context.Context, value string) error
|
||||
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
|
||||
InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error)
|
||||
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
|
||||
|
|
|
@ -60,6 +60,47 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds FROM api_keys WHERE last_used > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAPIKeysLastUsedAfter, lastUsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []APIKey
|
||||
for rows.Next() {
|
||||
var i APIKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.HashedSecret,
|
||||
&i.UserID,
|
||||
&i.LastUsed,
|
||||
&i.ExpiresAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LoginType,
|
||||
&i.OAuthAccessToken,
|
||||
&i.OAuthRefreshToken,
|
||||
&i.OAuthIDToken,
|
||||
&i.OAuthExpiry,
|
||||
&i.LifetimeSeconds,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertAPIKey = `-- name: InsertAPIKey :one
|
||||
INSERT INTO
|
||||
api_keys (
|
||||
|
@ -845,6 +886,50 @@ func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getParameterSchemasCreatedAfter = `-- name: GetParameterSchemasCreatedAfter :many
|
||||
SELECT id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type FROM parameter_schemas WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetParameterSchemasCreatedAfter(ctx context.Context, createdAt time.Time) ([]ParameterSchema, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getParameterSchemasCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ParameterSchema
|
||||
for rows.Next() {
|
||||
var i ParameterSchema
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.JobID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.DefaultSourceScheme,
|
||||
&i.DefaultSourceValue,
|
||||
&i.AllowOverrideSource,
|
||||
&i.DefaultDestinationScheme,
|
||||
&i.AllowOverrideDestination,
|
||||
&i.DefaultRefresh,
|
||||
&i.RedisplayValue,
|
||||
&i.ValidationError,
|
||||
&i.ValidationCondition,
|
||||
&i.ValidationTypeSystem,
|
||||
&i.ValidationValueType,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertParameterSchema = `-- name: InsertParameterSchema :one
|
||||
INSERT INTO
|
||||
parameter_schemas (
|
||||
|
@ -1470,6 +1555,49 @@ func (q *sqlQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUI
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many
|
||||
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id FROM provisioner_jobs WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProvisionerJobsCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ProvisionerJob
|
||||
for rows.Next() {
|
||||
var i ProvisionerJob
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.StartedAt,
|
||||
&i.CanceledAt,
|
||||
&i.CompletedAt,
|
||||
&i.Error,
|
||||
&i.OrganizationID,
|
||||
&i.InitiatorID,
|
||||
&i.Provisioner,
|
||||
&i.StorageMethod,
|
||||
&i.StorageSource,
|
||||
&i.Type,
|
||||
&i.Input,
|
||||
&i.WorkerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertProvisionerJob = `-- name: InsertProvisionerJob :one
|
||||
INSERT INTO
|
||||
provisioner_jobs (
|
||||
|
@ -1601,6 +1729,26 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a
|
|||
return err
|
||||
}
|
||||
|
||||
const getDeploymentID = `-- name: GetDeploymentID :one
|
||||
SELECT value FROM site_configs WHERE key = 'deployment_id'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetDeploymentID(ctx context.Context) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDeploymentID)
|
||||
var value string
|
||||
err := row.Scan(&value)
|
||||
return value, err
|
||||
}
|
||||
|
||||
const insertDeploymentID = `-- name: InsertDeploymentID :exec
|
||||
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) InsertDeploymentID(ctx context.Context, value string) error {
|
||||
_, err := q.db.ExecContext(ctx, insertDeploymentID, value)
|
||||
return err
|
||||
}
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
|
||||
|
@ -1671,6 +1819,46 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by FROM templates
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Template
|
||||
for rows.Next() {
|
||||
var i Template
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.CreatedBy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
|
||||
|
@ -2043,6 +2231,42 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
|
||||
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id FROM template_versions WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplateVersionsCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []TemplateVersion
|
||||
for rows.Next() {
|
||||
var i TemplateVersion
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TemplateID,
|
||||
&i.OrganizationID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Readme,
|
||||
&i.JobID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertTemplateVersion = `-- name: InsertTemplateVersion :one
|
||||
INSERT INTO
|
||||
template_versions (
|
||||
|
@ -2715,6 +2939,51 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
|
||||
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory FROM workspace_agents WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceAgent
|
||||
for rows.Next() {
|
||||
var i WorkspaceAgent
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
&i.Architecture,
|
||||
&i.EnvironmentVariables,
|
||||
&i.OperatingSystem,
|
||||
&i.StartupScript,
|
||||
&i.InstanceMetadata,
|
||||
&i.ResourceMetadata,
|
||||
&i.Directory,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one
|
||||
INSERT INTO
|
||||
workspace_agents (
|
||||
|
@ -2919,6 +3188,42 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceAppsCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceApp
|
||||
for rows.Next() {
|
||||
var i WorkspaceApp
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.AgentID,
|
||||
&i.Name,
|
||||
&i.Icon,
|
||||
&i.Command,
|
||||
&i.Url,
|
||||
&i.RelativePath,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspaceApp = `-- name: InsertWorkspaceApp :one
|
||||
INSERT INTO
|
||||
workspace_apps (
|
||||
|
@ -3270,6 +3575,46 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context,
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
|
||||
SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline FROM workspace_builds WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceBuild
|
||||
for rows.Next() {
|
||||
var i WorkspaceBuild
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.WorkspaceID,
|
||||
&i.TemplateVersionID,
|
||||
&i.Name,
|
||||
&i.BuildNumber,
|
||||
&i.Transition,
|
||||
&i.InitiatorID,
|
||||
&i.ProvisionerState,
|
||||
&i.JobID,
|
||||
&i.Deadline,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :one
|
||||
INSERT INTO
|
||||
workspace_builds (
|
||||
|
@ -3428,6 +3773,40 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many
|
||||
SELECT id, created_at, job_id, transition, type, name FROM workspace_resources WHERE created_at > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesCreatedAfter, createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceResource
|
||||
for rows.Next() {
|
||||
var i WorkspaceResource
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.JobID,
|
||||
&i.Transition,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one
|
||||
INSERT INTO
|
||||
workspace_resources (id, created_at, job_id, transition, type, name)
|
||||
|
@ -3570,56 +3949,7 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, i
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesAutostart = `-- name: GetWorkspacesAutostart :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
deleted = false
|
||||
AND
|
||||
(
|
||||
(autostart_schedule IS NOT NULL AND autostart_schedule <> '')
|
||||
OR
|
||||
(ttl IS NOT NULL AND ttl > 0)
|
||||
)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesAutostart(ctx context.Context) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesAutostart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesWithFilter = `-- name: GetWorkspacesWithFilter :many
|
||||
const getWorkspaces = `-- name: GetWorkspaces :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
||||
FROM
|
||||
|
@ -3661,7 +3991,7 @@ WHERE
|
|||
END
|
||||
`
|
||||
|
||||
type GetWorkspacesWithFilterParams struct {
|
||||
type GetWorkspacesParams struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
|
@ -3670,8 +4000,8 @@ type GetWorkspacesWithFilterParams struct {
|
|||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesWithFilter,
|
||||
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaces,
|
||||
arg.Deleted,
|
||||
arg.OwnerID,
|
||||
arg.OwnerUsername,
|
||||
|
@ -3711,6 +4041,55 @@ func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspa
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesAutostart = `-- name: GetWorkspacesAutostart :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
deleted = false
|
||||
AND
|
||||
(
|
||||
(autostart_schedule IS NOT NULL AND autostart_schedule <> '')
|
||||
OR
|
||||
(ttl IS NOT NULL AND ttl > 0)
|
||||
)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesAutostart(ctx context.Context) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesAutostart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspace = `-- name: InsertWorkspace :one
|
||||
INSERT INTO
|
||||
workspaces (
|
||||
|
|
|
@ -8,6 +8,9 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT * FROM api_keys WHERE last_used > $1;
|
||||
|
||||
-- name: InsertAPIKey :one
|
||||
INSERT INTO
|
||||
api_keys (
|
||||
|
|
|
@ -6,6 +6,9 @@ FROM
|
|||
WHERE
|
||||
job_id = $1;
|
||||
|
||||
-- name: GetParameterSchemasCreatedAfter :many
|
||||
SELECT * FROM parameter_schemas WHERE created_at > $1;
|
||||
|
||||
-- name: InsertParameterSchema :one
|
||||
INSERT INTO
|
||||
parameter_schemas (
|
||||
|
|
|
@ -46,6 +46,9 @@ FROM
|
|||
WHERE
|
||||
id = ANY(@ids :: uuid [ ]);
|
||||
|
||||
-- name: GetProvisionerJobsCreatedAfter :many
|
||||
SELECT * FROM provisioner_jobs WHERE created_at > $1;
|
||||
|
||||
-- name: InsertProvisionerJob :one
|
||||
INSERT INTO
|
||||
provisioner_jobs (
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- name: InsertDeploymentID :exec
|
||||
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1);
|
||||
|
||||
-- name: GetDeploymentID :one
|
||||
SELECT value FROM site_configs WHERE key = 'deployment_id';
|
|
@ -48,6 +48,9 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetTemplates :many
|
||||
SELECT * FROM templates;
|
||||
|
||||
-- name: InsertTemplate :one
|
||||
INSERT INTO
|
||||
templates (
|
||||
|
|
|
@ -40,6 +40,9 @@ FROM
|
|||
WHERE
|
||||
job_id = $1;
|
||||
|
||||
-- name: GetTemplateVersionsCreatedAfter :many
|
||||
SELECT * FROM template_versions WHERE created_at > $1;
|
||||
|
||||
-- name: GetTemplateVersionByTemplateIDAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
|
|
@ -34,6 +34,9 @@ FROM
|
|||
WHERE
|
||||
resource_id = ANY(@ids :: uuid [ ]);
|
||||
|
||||
-- name: GetWorkspaceAgentsCreatedAfter :many
|
||||
SELECT * FROM workspace_agents WHERE created_at > $1;
|
||||
|
||||
-- name: InsertWorkspaceAgent :one
|
||||
INSERT INTO
|
||||
workspace_agents (
|
||||
|
|
|
@ -7,6 +7,9 @@ SELECT * FROM workspace_apps WHERE agent_id = ANY(@ids :: uuid [ ]);
|
|||
-- name: GetWorkspaceAppByAgentIDAndName :one
|
||||
SELECT * FROM workspace_apps WHERE agent_id = $1 AND name = $2;
|
||||
|
||||
-- name: GetWorkspaceAppsCreatedAfter :many
|
||||
SELECT * FROM workspace_apps WHERE created_at > $1;
|
||||
|
||||
-- name: InsertWorkspaceApp :one
|
||||
INSERT INTO
|
||||
workspace_apps (
|
||||
|
|
|
@ -18,6 +18,9 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspaceBuildsCreatedAfter :many
|
||||
SELECT * FROM workspace_builds WHERE created_at > $1;
|
||||
|
||||
-- name: GetWorkspaceBuildByWorkspaceIDAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
|
|
@ -14,6 +14,9 @@ FROM
|
|||
WHERE
|
||||
job_id = $1;
|
||||
|
||||
-- name: GetWorkspaceResourcesCreatedAfter :many
|
||||
SELECT * FROM workspace_resources WHERE created_at > $1;
|
||||
|
||||
-- name: InsertWorkspaceResource :one
|
||||
INSERT INTO
|
||||
workspace_resources (id, created_at, job_id, transition, type, name)
|
||||
|
|
|
@ -8,7 +8,7 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspacesWithFilter :many
|
||||
-- name: GetWorkspaces :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
|
@ -80,6 +81,7 @@ func (api *API) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPCP
|
|||
Database: api.Database,
|
||||
Pubsub: api.Pubsub,
|
||||
Provisioners: daemon.Provisioners,
|
||||
Telemetry: api.Telemetry,
|
||||
Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -127,6 +129,7 @@ type provisionerdServer struct {
|
|||
Provisioners []database.ProvisionerType
|
||||
Database database.Store
|
||||
Pubsub database.Pubsub
|
||||
Telemetry telemetry.Reporter
|
||||
}
|
||||
|
||||
// AcquireJob queries the database to lock a job.
|
||||
|
@ -490,21 +493,28 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa
|
|||
if job.CompletedAt.Valid {
|
||||
return nil, xerrors.Errorf("job already completed")
|
||||
}
|
||||
job.CompletedAt = sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
job.Error = sql.NullString{
|
||||
String: failJob.Error,
|
||||
Valid: failJob.Error != "",
|
||||
}
|
||||
|
||||
err = server.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
|
||||
ID: jobID,
|
||||
CompletedAt: sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
UpdatedAt: database.Now(),
|
||||
Error: sql.NullString{
|
||||
String: failJob.Error,
|
||||
Valid: failJob.Error != "",
|
||||
},
|
||||
ID: jobID,
|
||||
CompletedAt: job.CompletedAt,
|
||||
UpdatedAt: database.Now(),
|
||||
Error: job.Error,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update provisioner job: %w", err)
|
||||
}
|
||||
server.Telemetry.Report(&telemetry.Snapshot{
|
||||
ProvisionerJobs: []telemetry.ProvisionerJob{telemetry.ConvertProvisionerJob(job)},
|
||||
})
|
||||
|
||||
switch jobType := failJob.Type.(type) {
|
||||
case *proto.FailedJob_WorkspaceBuild_:
|
||||
if jobType.WorkspaceBuild.State == nil {
|
||||
|
@ -543,6 +553,10 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
return nil, xerrors.Errorf("you don't have permission to update this job")
|
||||
}
|
||||
|
||||
telemetrySnapshot := &telemetry.Snapshot{}
|
||||
// Items are added to this snapshot as they complete!
|
||||
defer server.Telemetry.Report(telemetrySnapshot)
|
||||
|
||||
switch jobType := completed.Type.(type) {
|
||||
case *proto.CompletedJob_TemplateImport_:
|
||||
for transition, resources := range map[database.WorkspaceTransition][]*sdkproto.Resource{
|
||||
|
@ -556,7 +570,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
slog.F("resource_type", resource.Type),
|
||||
slog.F("transition", transition))
|
||||
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource)
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource, telemetrySnapshot)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert resource: %w", err)
|
||||
}
|
||||
|
@ -625,7 +639,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
}
|
||||
// This could be a bulk insert to improve performance.
|
||||
for _, protoResource := range jobType.WorkspaceBuild.Resources {
|
||||
err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource)
|
||||
err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource, telemetrySnapshot)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
|
@ -656,7 +670,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
slog.F("resource_name", resource.Name),
|
||||
slog.F("resource_type", resource.Type))
|
||||
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, database.WorkspaceTransitionStart, resource)
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, database.WorkspaceTransitionStart, resource, telemetrySnapshot)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert resource: %w", err)
|
||||
}
|
||||
|
@ -686,7 +700,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
return &proto.Empty{}, nil
|
||||
}
|
||||
|
||||
func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error {
|
||||
func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot) error {
|
||||
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
|
@ -698,6 +712,8 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job resource %q: %w", protoResource.Name, err)
|
||||
}
|
||||
snapshot.WorkspaceResources = append(snapshot.WorkspaceResources, telemetry.ConvertWorkspaceResource(resource))
|
||||
|
||||
for _, agent := range protoResource.Agents {
|
||||
var instanceID sql.NullString
|
||||
if agent.GetInstanceId() != "" {
|
||||
|
@ -745,9 +761,10 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
if err != nil {
|
||||
return xerrors.Errorf("insert agent: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent))
|
||||
|
||||
for _, app := range agent.Apps {
|
||||
_, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
AgentID: dbAgent.ID,
|
||||
|
@ -766,6 +783,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
if err != nil {
|
||||
return xerrors.Errorf("insert app: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceApps = append(snapshot.WorkspaceApps, telemetry.ConvertWorkspaceApp(dbApp))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -0,0 +1,721 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/buildinfo"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
)
|
||||
|
||||
const (
|
||||
// VersionHeader is sent in every telemetry request to
|
||||
// report the semantic version of Coder.
|
||||
VersionHeader = "X-Coder-Version"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Database database.Store
|
||||
Logger slog.Logger
|
||||
// URL is an endpoint to direct telemetry towards!
|
||||
URL *url.URL
|
||||
|
||||
BuiltinPostgres bool
|
||||
DeploymentID string
|
||||
GitHubOAuth bool
|
||||
Prometheus bool
|
||||
STUN bool
|
||||
SnapshotFrequency time.Duration
|
||||
Tunnel bool
|
||||
}
|
||||
|
||||
// New constructs a reporter for telemetry data.
|
||||
// Duplicate data will be sent, it's on the server-side to index by UUID.
|
||||
// Data is anonymized prior to being sent!
|
||||
func New(options Options) (Reporter, error) {
|
||||
if options.SnapshotFrequency == 0 {
|
||||
// Report once every 30mins by default!
|
||||
options.SnapshotFrequency = 30 * time.Minute
|
||||
}
|
||||
snapshotURL, err := options.URL.Parse("/snapshot")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse snapshot url: %w", err)
|
||||
}
|
||||
deploymentURL, err := options.URL.Parse("/deployment")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse deployment url: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
reporter := &remoteReporter{
|
||||
ctx: ctx,
|
||||
closed: make(chan struct{}),
|
||||
closeFunc: cancelFunc,
|
||||
options: options,
|
||||
deploymentURL: deploymentURL,
|
||||
snapshotURL: snapshotURL,
|
||||
startedAt: database.Now(),
|
||||
}
|
||||
go reporter.runSnapshotter()
|
||||
return reporter, nil
|
||||
}
|
||||
|
||||
// NewNoop creates a new telemetry reporter that entirely discards all requests.
|
||||
func NewNoop() Reporter {
|
||||
return &noopReporter{}
|
||||
}
|
||||
|
||||
// Reporter sends data to the telemetry server.
|
||||
type Reporter interface {
|
||||
// Report sends a snapshot to the telemetry server.
|
||||
// The contents of the snapshot can be a partial representation of the
|
||||
// database. For example, if a new user is added, a snapshot can
|
||||
// contain just that user entry.
|
||||
Report(snapshot *Snapshot)
|
||||
Close()
|
||||
}
|
||||
|
||||
type remoteReporter struct {
|
||||
ctx context.Context
|
||||
closed chan struct{}
|
||||
closeMutex sync.Mutex
|
||||
closeFunc context.CancelFunc
|
||||
|
||||
options Options
|
||||
deploymentURL,
|
||||
snapshotURL *url.URL
|
||||
startedAt time.Time
|
||||
shutdownAt *time.Time
|
||||
}
|
||||
|
||||
func (r *remoteReporter) Report(snapshot *Snapshot) {
|
||||
go r.reportSync(snapshot)
|
||||
}
|
||||
|
||||
func (r *remoteReporter) reportSync(snapshot *Snapshot) {
|
||||
snapshot.DeploymentID = r.options.DeploymentID
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
r.options.Logger.Error(r.ctx, "marshal snapshot: %w", slog.Error(err))
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.ctx, "POST", r.snapshotURL.String(), bytes.NewReader(data))
|
||||
if err != nil {
|
||||
r.options.Logger.Error(r.ctx, "create request", slog.Error(err))
|
||||
return
|
||||
}
|
||||
req.Header.Set(VersionHeader, buildinfo.Version())
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
// If the request fails it's not necessarily an error.
|
||||
// In an airgapped environment, it's fine if this fails!
|
||||
r.options.Logger.Debug(r.ctx, "submit", slog.Error(err))
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
r.options.Logger.Debug(r.ctx, "bad response from telemetry server", slog.F("status", resp.StatusCode))
|
||||
return
|
||||
}
|
||||
r.options.Logger.Debug(r.ctx, "submitted snapshot")
|
||||
}
|
||||
|
||||
func (r *remoteReporter) Close() {
|
||||
r.closeMutex.Lock()
|
||||
defer r.closeMutex.Unlock()
|
||||
if r.isClosed() {
|
||||
return
|
||||
}
|
||||
close(r.closed)
|
||||
now := database.Now()
|
||||
r.shutdownAt = &now
|
||||
// Report a final collection of telemetry prior to close!
|
||||
// This could indicate final actions a user has taken, and
|
||||
// the time the deployment was shutdown.
|
||||
r.reportWithDeployment()
|
||||
r.closeFunc()
|
||||
}
|
||||
|
||||
func (r *remoteReporter) isClosed() bool {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *remoteReporter) runSnapshotter() {
|
||||
first := true
|
||||
ticker := time.NewTicker(r.options.SnapshotFrequency)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
if !first {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
// Skip the ticker on the first run to report instantly!
|
||||
}
|
||||
first = false
|
||||
r.closeMutex.Lock()
|
||||
if r.isClosed() {
|
||||
r.closeMutex.Unlock()
|
||||
return
|
||||
}
|
||||
r.reportWithDeployment()
|
||||
r.closeMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *remoteReporter) reportWithDeployment() {
|
||||
// Submit deployment information before creating a snapshot!
|
||||
// This is separated from the snapshot API call to reduce
|
||||
// duplicate data from being inserted. Snapshot may be called
|
||||
// numerous times simaltanously if there is lots of activity!
|
||||
err := r.deployment()
|
||||
if err != nil {
|
||||
r.options.Logger.Debug(r.ctx, "update deployment", slog.Error(err))
|
||||
return
|
||||
}
|
||||
snapshot, err := r.createSnapshot()
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
r.options.Logger.Error(r.ctx, "create snapshot", slog.Error(err))
|
||||
return
|
||||
}
|
||||
r.reportSync(snapshot)
|
||||
}
|
||||
|
||||
// deployment collects host information and reports it to the telemetry server.
|
||||
func (r *remoteReporter) deployment() error {
|
||||
sysInfoHost, err := sysinfo.Host()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get host info: %w", err)
|
||||
}
|
||||
mem, err := sysInfoHost.Memory()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get memory info: %w", err)
|
||||
}
|
||||
sysInfo := sysInfoHost.Info()
|
||||
|
||||
containerized := false
|
||||
if sysInfo.Containerized != nil {
|
||||
containerized = *sysInfo.Containerized
|
||||
}
|
||||
data, err := json.Marshal(&Deployment{
|
||||
ID: r.options.DeploymentID,
|
||||
Architecture: sysInfo.Architecture,
|
||||
BuiltinPostgres: r.options.BuiltinPostgres,
|
||||
Containerized: containerized,
|
||||
GitHubOAuth: r.options.GitHubOAuth,
|
||||
Prometheus: r.options.Prometheus,
|
||||
STUN: r.options.STUN,
|
||||
Tunnel: r.options.Tunnel,
|
||||
OSType: sysInfo.OS.Type,
|
||||
OSFamily: sysInfo.OS.Family,
|
||||
OSPlatform: sysInfo.OS.Platform,
|
||||
OSName: sysInfo.OS.Name,
|
||||
OSVersion: sysInfo.OS.Version,
|
||||
CPUCores: runtime.NumCPU(),
|
||||
MemoryTotal: mem.Total,
|
||||
MachineID: sysInfo.UniqueID,
|
||||
StartedAt: r.startedAt,
|
||||
ShutdownAt: r.shutdownAt,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal deployment: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.ctx, "POST", r.deploymentURL.String(), bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create deployment request: %w", err)
|
||||
}
|
||||
req.Header.Set(VersionHeader, buildinfo.Version())
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("perform request: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return xerrors.Errorf("update deployment: %w", err)
|
||||
}
|
||||
r.options.Logger.Debug(r.ctx, "submitted deployment info")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSnapshot collects a full snapshot from the database.
|
||||
func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
|
||||
var (
|
||||
ctx = r.ctx
|
||||
// For resources that grow in size very quickly (like workspace builds),
|
||||
// we only report events that occurred within the past hour.
|
||||
createdAfter = database.Now().Add(-1 * time.Hour)
|
||||
eg errgroup.Group
|
||||
snapshot = &Snapshot{
|
||||
DeploymentID: r.options.DeploymentID,
|
||||
}
|
||||
)
|
||||
|
||||
eg.Go(func() error {
|
||||
apiKeys, err := r.options.Database.GetAPIKeysLastUsedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get api keys last used: %w", err)
|
||||
}
|
||||
snapshot.APIKeys = make([]APIKey, 0, len(apiKeys))
|
||||
for _, apiKey := range apiKeys {
|
||||
snapshot.APIKeys = append(snapshot.APIKeys, ConvertAPIKey(apiKey))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
schemas, err := r.options.Database.GetParameterSchemasCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get parameter schemas: %w", err)
|
||||
}
|
||||
snapshot.ParameterSchemas = make([]ParameterSchema, 0, len(schemas))
|
||||
for _, schema := range schemas {
|
||||
snapshot.ParameterSchemas = append(snapshot.ParameterSchemas, ParameterSchema{
|
||||
ID: schema.ID,
|
||||
JobID: schema.JobID,
|
||||
Name: schema.Name,
|
||||
ValidationCondition: schema.ValidationCondition,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
jobs, err := r.options.Database.GetProvisionerJobsCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get provisioner jobs: %w", err)
|
||||
}
|
||||
snapshot.ProvisionerJobs = make([]ProvisionerJob, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
snapshot.ProvisionerJobs = append(snapshot.ProvisionerJobs, ConvertProvisionerJob(job))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
templates, err := r.options.Database.GetTemplates(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get templates: %w", err)
|
||||
}
|
||||
snapshot.Templates = make([]Template, 0, len(templates))
|
||||
for _, dbTemplate := range templates {
|
||||
snapshot.Templates = append(snapshot.Templates, ConvertTemplate(dbTemplate))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
templateVersions, err := r.options.Database.GetTemplateVersionsCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template versions: %w", err)
|
||||
}
|
||||
snapshot.TemplateVersions = make([]TemplateVersion, 0, len(templateVersions))
|
||||
for _, version := range templateVersions {
|
||||
snapshot.TemplateVersions = append(snapshot.TemplateVersions, ConvertTemplateVersion(version))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
users, err := r.options.Database.GetUsers(ctx, database.GetUsersParams{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get users: %w", err)
|
||||
}
|
||||
var firstUser database.User
|
||||
for _, dbUser := range users {
|
||||
if dbUser.Status != database.UserStatusActive {
|
||||
continue
|
||||
}
|
||||
if firstUser.CreatedAt.IsZero() {
|
||||
firstUser = dbUser
|
||||
}
|
||||
if dbUser.CreatedAt.After(firstUser.CreatedAt) {
|
||||
firstUser = dbUser
|
||||
}
|
||||
}
|
||||
snapshot.Users = make([]User, 0, len(users))
|
||||
for _, dbUser := range users {
|
||||
user := ConvertUser(dbUser)
|
||||
// If it's the first user, we'll send the email!
|
||||
if firstUser.ID == dbUser.ID {
|
||||
email := dbUser.Email
|
||||
user.Email = &email
|
||||
}
|
||||
snapshot.Users = append(snapshot.Users, user)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
workspaces, err := r.options.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspaces: %w", err)
|
||||
}
|
||||
snapshot.Workspaces = make([]Workspace, 0, len(workspaces))
|
||||
for _, dbWorkspace := range workspaces {
|
||||
snapshot.Workspaces = append(snapshot.Workspaces, ConvertWorkspace(dbWorkspace))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
workspaceApps, err := r.options.Database.GetWorkspaceAppsCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace apps: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceApps = make([]WorkspaceApp, 0, len(workspaceApps))
|
||||
for _, app := range workspaceApps {
|
||||
snapshot.WorkspaceApps = append(snapshot.WorkspaceApps, ConvertWorkspaceApp(app))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
workspaceAgents, err := r.options.Database.GetWorkspaceAgentsCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace agents: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceAgents = make([]WorkspaceAgent, 0, len(workspaceAgents))
|
||||
for _, agent := range workspaceAgents {
|
||||
snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, ConvertWorkspaceAgent(agent))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
workspaceBuilds, err := r.options.Database.GetWorkspaceBuildsCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace builds: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceBuilds = make([]WorkspaceBuild, 0, len(workspaceBuilds))
|
||||
for _, build := range workspaceBuilds {
|
||||
snapshot.WorkspaceBuilds = append(snapshot.WorkspaceBuilds, ConvertWorkspaceBuild(build))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
workspaceResources, err := r.options.Database.GetWorkspaceResourcesCreatedAfter(ctx, createdAfter)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace resources: %w", err)
|
||||
}
|
||||
snapshot.WorkspaceResources = make([]WorkspaceResource, 0, len(workspaceResources))
|
||||
for _, resource := range workspaceResources {
|
||||
snapshot.WorkspaceResources = append(snapshot.WorkspaceResources, ConvertWorkspaceResource(resource))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := eg.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// ConvertAPIKey anonymizes an API key.
|
||||
func ConvertAPIKey(apiKey database.APIKey) APIKey {
|
||||
return APIKey{
|
||||
ID: apiKey.ID,
|
||||
UserID: apiKey.UserID,
|
||||
CreatedAt: apiKey.CreatedAt,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
LoginType: apiKey.LoginType,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertWorkspace anonymizes a workspace.
|
||||
func ConvertWorkspace(workspace database.Workspace) Workspace {
|
||||
return Workspace{
|
||||
ID: workspace.ID,
|
||||
OrganizationID: workspace.OrganizationID,
|
||||
OwnerID: workspace.OwnerID,
|
||||
TemplateID: workspace.TemplateID,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
Deleted: workspace.Deleted,
|
||||
Name: workspace.Name,
|
||||
AutostartSchedule: workspace.AutostartSchedule.String,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertWorkspaceBuild anonymizes a workspace build.
|
||||
func ConvertWorkspaceBuild(build database.WorkspaceBuild) WorkspaceBuild {
|
||||
return WorkspaceBuild{
|
||||
ID: build.ID,
|
||||
CreatedAt: build.CreatedAt,
|
||||
WorkspaceID: build.WorkspaceID,
|
||||
JobID: build.JobID,
|
||||
TemplateVersionID: build.TemplateVersionID,
|
||||
BuildNumber: uint32(build.BuildNumber),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertProvisionerJob anonymizes a provisioner job.
|
||||
func ConvertProvisionerJob(job database.ProvisionerJob) ProvisionerJob {
|
||||
snapJob := ProvisionerJob{
|
||||
ID: job.ID,
|
||||
OrganizationID: job.OrganizationID,
|
||||
InitiatorID: job.InitiatorID,
|
||||
CreatedAt: job.CreatedAt,
|
||||
UpdatedAt: job.UpdatedAt,
|
||||
Error: job.Error.String,
|
||||
Type: job.Type,
|
||||
}
|
||||
if job.StartedAt.Valid {
|
||||
snapJob.StartedAt = &job.StartedAt.Time
|
||||
}
|
||||
if job.CanceledAt.Valid {
|
||||
snapJob.CanceledAt = &job.CanceledAt.Time
|
||||
}
|
||||
if job.CompletedAt.Valid {
|
||||
snapJob.CompletedAt = &job.CompletedAt.Time
|
||||
}
|
||||
return snapJob
|
||||
}
|
||||
|
||||
// ConvertWorkspaceAgent anonymizes a workspace agent.
|
||||
func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
|
||||
return WorkspaceAgent{
|
||||
ID: agent.ID,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
ResourceID: agent.ResourceID,
|
||||
InstanceAuth: agent.AuthInstanceID.Valid,
|
||||
Architecture: agent.Architecture,
|
||||
OperatingSystem: agent.OperatingSystem,
|
||||
EnvironmentVariables: agent.EnvironmentVariables.Valid,
|
||||
StartupScript: agent.StartupScript.Valid,
|
||||
Directory: agent.Directory != "",
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertWorkspaceApp anonymizes a workspace app.
|
||||
func ConvertWorkspaceApp(app database.WorkspaceApp) WorkspaceApp {
|
||||
return WorkspaceApp{
|
||||
ID: app.ID,
|
||||
CreatedAt: app.CreatedAt,
|
||||
AgentID: app.AgentID,
|
||||
Icon: app.Icon,
|
||||
RelativePath: app.RelativePath,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertWorkspaceResource anonymizes a workspace resource.
|
||||
func ConvertWorkspaceResource(resource database.WorkspaceResource) WorkspaceResource {
|
||||
return WorkspaceResource{
|
||||
ID: resource.ID,
|
||||
JobID: resource.JobID,
|
||||
Transition: resource.Transition,
|
||||
Type: resource.Type,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUser anonymizes a user.
|
||||
func ConvertUser(dbUser database.User) User {
|
||||
emailHashed := ""
|
||||
atSymbol := strings.LastIndex(dbUser.Email, "@")
|
||||
if atSymbol >= 0 {
|
||||
// We hash the beginning of the user to allow for indexing users
|
||||
// by email between deployments.
|
||||
hash := sha256.Sum256([]byte(dbUser.Email[:atSymbol]))
|
||||
emailHashed = fmt.Sprintf("%x%s", hash[:], dbUser.Email[atSymbol:])
|
||||
}
|
||||
return User{
|
||||
ID: dbUser.ID,
|
||||
EmailHashed: emailHashed,
|
||||
RBACRoles: dbUser.RBACRoles,
|
||||
CreatedAt: dbUser.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertTemplate anonymizes a template.
|
||||
func ConvertTemplate(dbTemplate database.Template) Template {
|
||||
return Template{
|
||||
ID: dbTemplate.ID,
|
||||
CreatedBy: dbTemplate.CreatedBy,
|
||||
CreatedAt: dbTemplate.CreatedAt,
|
||||
UpdatedAt: dbTemplate.UpdatedAt,
|
||||
OrganizationID: dbTemplate.OrganizationID,
|
||||
Deleted: dbTemplate.Deleted,
|
||||
ActiveVersionID: dbTemplate.ActiveVersionID,
|
||||
Name: dbTemplate.Name,
|
||||
Description: dbTemplate.Description != "",
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertTemplateVersion anonymizes a template version.
|
||||
func ConvertTemplateVersion(version database.TemplateVersion) TemplateVersion {
|
||||
snapVersion := TemplateVersion{
|
||||
ID: version.ID,
|
||||
CreatedAt: version.CreatedAt,
|
||||
OrganizationID: version.OrganizationID,
|
||||
JobID: version.JobID,
|
||||
}
|
||||
if version.TemplateID.Valid {
|
||||
snapVersion.TemplateID = &version.TemplateID.UUID
|
||||
}
|
||||
return snapVersion
|
||||
}
|
||||
|
||||
// Snapshot represents a point-in-time anonymized database dump.
|
||||
// Data is aggregated by latest on the server-side, so partial data
|
||||
// can be sent without issue.
|
||||
type Snapshot struct {
|
||||
DeploymentID string `json:"deployment_id"`
|
||||
|
||||
APIKeys []APIKey `json:"api_keys"`
|
||||
ParameterSchemas []ParameterSchema `json:"parameter_schemas"`
|
||||
ProvisionerJobs []ProvisionerJob `json:"provisioner_jobs"`
|
||||
Templates []Template `json:"templates"`
|
||||
TemplateVersions []TemplateVersion `json:"template_versions"`
|
||||
Users []User `json:"users"`
|
||||
Workspaces []Workspace `json:"workspaces"`
|
||||
WorkspaceApps []WorkspaceApp `json:"workspace_apps"`
|
||||
WorkspaceAgents []WorkspaceAgent `json:"workspace_agents"`
|
||||
WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"`
|
||||
WorkspaceResources []WorkspaceResource `json:"workspace_resources"`
|
||||
}
|
||||
|
||||
// Deployment contains information about the host running Coder.
|
||||
type Deployment struct {
|
||||
ID string `json:"id"`
|
||||
Architecture string `json:"architecture"`
|
||||
BuiltinPostgres bool `json:"builtin_postgres"`
|
||||
Containerized bool `json:"containerized"`
|
||||
Tunnel bool `json:"tunnel"`
|
||||
GitHubOAuth bool `json:"github_oauth"`
|
||||
Prometheus bool `json:"prometheus"`
|
||||
STUN bool `json:"stun"`
|
||||
OSType string `json:"os_type"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSPlatform string `json:"os_platform"`
|
||||
OSName string `json:"os_name"`
|
||||
OSVersion string `json:"os_version"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
MemoryTotal uint64 `json:"memory_total"`
|
||||
MachineID string `json:"machine_id"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
ShutdownAt *time.Time `json:"shutdown_at"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed time.Time `json:"last_used"`
|
||||
LoginType database.LoginType `json:"login_type"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// Email is only filled in for the first/admin user!
|
||||
Email *string `json:"email"`
|
||||
EmailHashed string `json:"email_hashed"`
|
||||
RBACRoles []string `json:"rbac_roles"`
|
||||
Status database.UserStatus `json:"status"`
|
||||
}
|
||||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ResourceID uuid.UUID `json:"resource_id"`
|
||||
InstanceAuth bool `json:"instance_auth"`
|
||||
Architecture string `json:"architecture"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
EnvironmentVariables bool `json:"environment_variables"`
|
||||
StartupScript bool `json:"startup_script"`
|
||||
Directory bool `json:"directory"`
|
||||
}
|
||||
|
||||
type WorkspaceApp struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
Icon string `json:"icon"`
|
||||
RelativePath bool `json:"relative_path"`
|
||||
}
|
||||
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
BuildNumber uint32 `json:"build_number"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OwnerID uuid.UUID `json:"owner_id"`
|
||||
TemplateID uuid.UUID `json:"template_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Name string `json:"name"`
|
||||
AutostartSchedule string `json:"autostart_schedule"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Deleted bool `json:"deleted"`
|
||||
ActiveVersionID uuid.UUID `json:"active_version_id"`
|
||||
Name string `json:"name"`
|
||||
Description bool `json:"description"`
|
||||
}
|
||||
|
||||
type TemplateVersion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
TemplateID *uuid.UUID `json:"template_id,omitempty"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
}
|
||||
|
||||
type ProvisionerJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
InitiatorID uuid.UUID `json:"initiator_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CanceledAt *time.Time `json:"canceled_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Error string `json:"error"`
|
||||
Type database.ProvisionerJobType `json:"type"`
|
||||
}
|
||||
|
||||
type ParameterSchema struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Name string `json:"name"`
|
||||
ValidationCondition string `json:"validation_condition"`
|
||||
}
|
||||
|
||||
type noopReporter struct{}
|
||||
|
||||
func (*noopReporter) Report(_ *Snapshot) {}
|
||||
func (*noopReporter) Close() {}
|
|
@ -0,0 +1,148 @@
|
|||
package telemetry_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/buildinfo"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestTelemetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Snapshot", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
ctx := context.Background()
|
||||
_, err := db.InsertAPIKey(ctx, database.InsertAPIKeyParams{
|
||||
ID: uuid.NewString(),
|
||||
LastUsed: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertParameterSchema(ctx, database.InsertParameterSchemaParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertTemplate(ctx, database.InsertTemplateParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertUser(ctx, database.InsertUserParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
snapshot := collectSnapshot(t, db)
|
||||
require.Len(t, snapshot.ParameterSchemas, 1)
|
||||
require.Len(t, snapshot.ProvisionerJobs, 1)
|
||||
require.Len(t, snapshot.Templates, 1)
|
||||
require.Len(t, snapshot.TemplateVersions, 1)
|
||||
require.Len(t, snapshot.Users, 1)
|
||||
require.Len(t, snapshot.Workspaces, 1)
|
||||
require.Len(t, snapshot.WorkspaceApps, 1)
|
||||
require.Len(t, snapshot.WorkspaceAgents, 1)
|
||||
require.Len(t, snapshot.WorkspaceBuilds, 1)
|
||||
require.Len(t, snapshot.WorkspaceResources, 1)
|
||||
})
|
||||
t.Run("HashedEmail", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
_, err := db.InsertUser(context.Background(), database.InsertUserParams{
|
||||
ID: uuid.New(),
|
||||
Email: "kyle@coder.com",
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
snapshot := collectSnapshot(t, db)
|
||||
require.Len(t, snapshot.Users, 1)
|
||||
require.Equal(t, snapshot.Users[0].EmailHashed, "bb44bf07cf9a2db0554bba63a03d822c927deae77df101874496df5a6a3e896d@coder.com")
|
||||
})
|
||||
}
|
||||
|
||||
func collectSnapshot(t *testing.T, db database.Store) *telemetry.Snapshot {
|
||||
t.Helper()
|
||||
deployment := make(chan struct{}, 64)
|
||||
snapshot := make(chan *telemetry.Snapshot, 64)
|
||||
r := chi.NewRouter()
|
||||
r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader))
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
deployment <- struct{}{}
|
||||
})
|
||||
r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader))
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
ss := &telemetry.Snapshot{}
|
||||
err := json.NewDecoder(r.Body).Decode(ss)
|
||||
require.NoError(t, err)
|
||||
snapshot <- ss
|
||||
})
|
||||
server := httptest.NewServer(r)
|
||||
t.Cleanup(server.Close)
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
reporter, err := telemetry.New(telemetry.Options{
|
||||
Database: db,
|
||||
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
||||
URL: serverURL,
|
||||
DeploymentID: uuid.NewString(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(reporter.Close)
|
||||
<-deployment
|
||||
return <-snapshot
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
@ -75,7 +76,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
|
||||
workspaces, err := api.Database.GetWorkspaces(r.Context(), database.GetWorkspacesParams{
|
||||
TemplateIds: []uuid.UUID{template.ID},
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
@ -180,10 +181,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||
minAutostartInterval = time.Duration(*createTemplate.MinAutostartIntervalMillis) * time.Millisecond
|
||||
}
|
||||
|
||||
var dbTemplate database.Template
|
||||
var template codersdk.Template
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := database.Now()
|
||||
dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{
|
||||
dbTemplate, err = db.InsertTemplate(r.Context(), database.InsertTemplateParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
@ -244,6 +246,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Templates: []telemetry.Template{telemetry.ConvertTemplate(dbTemplate)},
|
||||
TemplateVersions: []telemetry.TemplateVersion{telemetry.ConvertTemplateVersion(templateVersion)},
|
||||
})
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, template)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/userpassword"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
|
@ -86,6 +87,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
telemetryUser := telemetry.ConvertUser(user)
|
||||
// Send the initial users email address!
|
||||
telemetryUser.Email = &user.Email
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Users: []telemetry.User{telemetryUser},
|
||||
})
|
||||
|
||||
// TODO: @emyrk this currently happens outside the database tx used to create
|
||||
// the user. Maybe I add this ability to grant roles in the createUser api
|
||||
// and add some rbac bypass when calling api functions this way??
|
||||
|
@ -252,6 +260,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Report when users are added!
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Users: []telemetry.User{telemetry.ConvertUser(user)},
|
||||
})
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{createUser.OrganizationID}))
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
@ -123,7 +124,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
|||
filter.OwnerUsername = ""
|
||||
}
|
||||
|
||||
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter)
|
||||
workspaces, err := api.Database.GetWorkspaces(r.Context(), filter)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "Internal error fetching workspaces.",
|
||||
|
@ -457,6 +458,11 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Workspaces: []telemetry.Workspace{telemetry.ConvertWorkspace(workspace)},
|
||||
WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(workspaceBuild)},
|
||||
})
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, workspaceBuild, templateVersionJob, template,
|
||||
findUser(apiKey.UserID, users), findUser(workspaceBuild.InitiatorID, users)))
|
||||
}
|
||||
|
@ -945,11 +951,11 @@ func validWorkspaceSchedule(s *string, min time.Duration) (sql.NullString, error
|
|||
|
||||
// workspaceSearchQuery takes a query string and returns the workspace filter.
|
||||
// It also can return the list of validation errors to return to the api.
|
||||
func workspaceSearchQuery(query string) (database.GetWorkspacesWithFilterParams, []httpapi.Error) {
|
||||
func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []httpapi.Error) {
|
||||
searchParams := make(url.Values)
|
||||
if query == "" {
|
||||
// No filter
|
||||
return database.GetWorkspacesWithFilterParams{}, nil
|
||||
return database.GetWorkspacesParams{}, nil
|
||||
}
|
||||
// Because we do this in 2 passes, we want to maintain quotes on the first
|
||||
// pass.Further splitting occurs on the second pass and quotes will be
|
||||
|
@ -968,14 +974,14 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesWithFilterParams,
|
|||
searchParams.Set("owner", parts[0])
|
||||
searchParams.Set("name", parts[1])
|
||||
default:
|
||||
return database.GetWorkspacesWithFilterParams{}, []httpapi.Error{
|
||||
return database.GetWorkspacesParams{}, []httpapi.Error{
|
||||
{Field: "q", Detail: fmt.Sprintf("Query element %q can only contain 1 '/'", element)},
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
searchParams.Set(parts[0], parts[1])
|
||||
default:
|
||||
return database.GetWorkspacesWithFilterParams{}, []httpapi.Error{
|
||||
return database.GetWorkspacesParams{}, []httpapi.Error{
|
||||
{Field: "q", Detail: fmt.Sprintf("Query element %q can only contain 1 ':'", element)},
|
||||
}
|
||||
}
|
||||
|
@ -984,7 +990,7 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesWithFilterParams,
|
|||
// Using the query param parser here just returns consistent errors with
|
||||
// other parsing.
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter := database.GetWorkspacesWithFilterParams{
|
||||
filter := database.GetWorkspacesParams{
|
||||
Deleted: false,
|
||||
OwnerUsername: parser.String(searchParams, "", "owner"),
|
||||
TemplateName: parser.String(searchParams, "", "template"),
|
||||
|
|
|
@ -15,18 +15,18 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
testCases := []struct {
|
||||
Name string
|
||||
Query string
|
||||
Expected database.GetWorkspacesWithFilterParams
|
||||
Expected database.GetWorkspacesParams
|
||||
ExpectedErrorContains string
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
Query: "",
|
||||
Expected: database.GetWorkspacesWithFilterParams{},
|
||||
Expected: database.GetWorkspacesParams{},
|
||||
},
|
||||
{
|
||||
Name: "Owner/Name",
|
||||
Query: "Foo/Bar",
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
OwnerUsername: "Foo",
|
||||
Name: "Bar",
|
||||
},
|
||||
|
@ -34,14 +34,14 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
{
|
||||
Name: "Name",
|
||||
Query: "workspace-name",
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "workspace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Name+Param",
|
||||
Query: "workspace-name template:docker",
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "workspace-name",
|
||||
TemplateName: "docker",
|
||||
},
|
||||
|
@ -49,7 +49,7 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
{
|
||||
Name: "OnlyParams",
|
||||
Query: "name:workspace-name template:docker owner:alice",
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "workspace-name",
|
||||
TemplateName: "docker",
|
||||
OwnerUsername: "alice",
|
||||
|
@ -58,7 +58,7 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
{
|
||||
Name: "QuotedParam",
|
||||
Query: `name:workspace-name template:"docker template" owner:alice`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "workspace-name",
|
||||
TemplateName: "docker template",
|
||||
OwnerUsername: "alice",
|
||||
|
@ -67,7 +67,7 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
{
|
||||
Name: "QuotedKey",
|
||||
Query: `"name":baz "template":foo "owner":bar`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "baz",
|
||||
TemplateName: "foo",
|
||||
OwnerUsername: "bar",
|
||||
|
@ -77,34 +77,34 @@ func TestSearchWorkspace(t *testing.T) {
|
|||
// This will not return an error
|
||||
Name: "ExtraKeys",
|
||||
Query: `foo:bar`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{},
|
||||
Expected: database.GetWorkspacesParams{},
|
||||
},
|
||||
{
|
||||
// Quotes keep elements together
|
||||
Name: "QuotedSpecial",
|
||||
Query: `name:"workspace:name"`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "workspace:name",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "QuotedMadness",
|
||||
Query: `"name":"foo:bar:baz/baz/zoo:zonk"`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "foo:bar:baz/baz/zoo:zonk",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "QuotedName",
|
||||
Query: `"foo/bar"`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "foo/bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "QuotedOwner/Name",
|
||||
Query: `"foo"/"bar"`,
|
||||
Expected: database.GetWorkspacesWithFilterParams{
|
||||
Expected: database.GetWorkspacesParams{
|
||||
Name: "bar",
|
||||
OwnerUsername: "foo",
|
||||
},
|
||||
|
|
6
go.mod
6
go.mod
|
@ -54,6 +54,7 @@ require (
|
|||
github.com/coder/retry v1.3.0
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/elastic/go-sysinfo v1.8.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fergusstrange/embedded-postgres v1.16.0
|
||||
|
@ -134,9 +135,11 @@ require github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
|||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.0.1 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/elastic/go-windows v1.0.0 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.4.4 // indirect
|
||||
github.com/yuin/goldmark v1.4.12 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -234,7 +237,6 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.0 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.4.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
|
|
10
go.sum
10
go.sum
|
@ -516,6 +516,10 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
|
|||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/elastic/go-sysinfo v1.8.0 h1:hwmVlZLfTVTP+L0hSS2BD/G8GNPmcl4JEMoOktSw/wc=
|
||||
github.com/elastic/go-sysinfo v1.8.0/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY=
|
||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
|
@ -984,6 +988,7 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
|
|||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.3.2 h1:+46BKrPFAyhAn3MTT3vzvZc+qvWAX23yviAlBG9zAxA=
|
||||
github.com/jedib0t/go-pretty/v6 v6.3.2/go.mod h1:B1WBBWnJhW9jnk7GHxY+p9NlmNwf/KUb4hKsRk6BdBQ=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
|
@ -996,6 +1001,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
|||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
|
@ -1510,7 +1517,6 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
|
|||
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
|
@ -2438,6 +2444,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
|||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.1/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
|
||||
k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
|
||||
k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
|
||||
|
|
Loading…
Reference in New Issue