From fb4c1d7cd10d75447e2162c5886e70abd49a3160 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 18:50:52 -0600 Subject: [PATCH 1/6] Enable more linting rules --- .golangci.yml | 27 +++++++++++++++++++++++++++ coder-sdk/error.go | 6 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index daa86752..f97bc7e5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,6 +17,7 @@ linters-settings: - (cdr.dev/coder-cli/pkg/clog).Causef linters: disable-all: true + exclude-use-default: false enable: - megacheck - govet @@ -44,3 +45,29 @@ linters: - rowserrcheck - scopelint - goprintffuncname + - gofmt + - godot + - ineffassign + - gocritic + +issues: + exclude-use-default: false + exclude: + # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked + # golint: False positive when tests are defined in package 'test' + - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this + # govet: Common false positives + - (possible misuse of unsafe.Pointer|should have signature) + # staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore + - ineffective break statement. Did you mean to break out of the outer loop + # gosec: Too many false-positives on 'unsafe' usage + - Use of unsafe calls should be audited + # gosec: Too many false-positives for parametrized shell calls + - Subprocess launch(ed with variable|ing should be audited) + # gosec: Duplicated errcheck checks + - G104 + # gosec: Too many issues in popular repos + - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) + # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' + - Potential file inclusion via variable \ No newline at end of file diff --git a/coder-sdk/error.go b/coder-sdk/error.go index 2b312378..75667ef1 100644 --- a/coder-sdk/error.go +++ b/coder-sdk/error.go @@ -10,7 +10,11 @@ import ( ) // ErrNotFound describes an error case in which the requested resource could not be found -var ErrNotFound = xerrors.Errorf("resource not found") +var ErrNotFound = xerrors.New("resource not found") + +var ErrPermissions = xerrors.New("insufficient permissions") + +var ErrAuthentication = xerrors.New("invalid authentication") // APIError is the expected payload format for our errors. type APIError struct { From febead108ac2871004fc60996532400e4b9987ef Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 20:04:45 -0600 Subject: [PATCH 2/6] fixup! Enable more linting rules --- .golangci.yml | 1 - coder-sdk/client.go | 4 ++-- coder-sdk/config.go | 18 ++++++++++++++ coder-sdk/env.go | 14 +++++------ coder-sdk/error.go | 4 +++- coder-sdk/image.go | 8 +++---- coder-sdk/org.go | 16 +++++++++---- coder-sdk/secrets.go | 10 ++++---- coder-sdk/tokens.go | 7 ++++++ coder-sdk/users.go | 13 ++++++---- coder-sdk/util.go | 1 + pkg/tcli/tcli.go | 56 +++++++++++++++++++++++--------------------- 12 files changed, 96 insertions(+), 56 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f97bc7e5..bfe48ebf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters-settings: min-complexity: 46 nestif: min-complexity: 10 - golint: govet: settings: printf: diff --git a/coder-sdk/client.go b/coder-sdk/client.go index 959c7611..12a7c476 100644 --- a/coder-sdk/client.go +++ b/coder-sdk/client.go @@ -6,10 +6,10 @@ import ( "net/url" ) -// Me is the route param to access resources of the authenticated user +// Me is the route param to access resources of the authenticated user. const Me = "me" -// Client wraps the Coder HTTP API +// Client wraps the Coder HTTP API. type Client struct { BaseURL *url.URL Token string diff --git a/coder-sdk/config.go b/coder-sdk/config.go index e4a741ad..7912e78a 100644 --- a/coder-sdk/config.go +++ b/coder-sdk/config.go @@ -5,6 +5,7 @@ import ( "net/http" ) +// AuthProviderType is an enum of each valid auth provider. type AuthProviderType string // AuthProviderType enum. @@ -14,18 +15,21 @@ const ( AuthProviderOIDC AuthProviderType = "oidc" ) +// ConfigAuth describes the authentication configuration for a Coder Enterprise deployment. type ConfigAuth struct { ProviderType *AuthProviderType `json:"provider_type"` OIDC *ConfigOIDC `json:"oidc"` SAML *ConfigSAML `json:"saml"` } +// ConfigOIDC describes the OIDC configuration for single-signon support in Coder Enterprise. type ConfigOIDC struct { ClientID *string `json:"client_id"` ClientSecret *string `json:"client_secret"` Issuer *string `json:"issuer"` } +// ConfigSAML describes the SAML configuration values. type ConfigSAML struct { IdentityProviderMetadataURL *string `json:"idp_metadata_url"` SignatureAlgorithm *string `json:"signature_algorithm"` @@ -34,28 +38,33 @@ type ConfigSAML struct { PublicKeyCertificate *string `json:"public_key_certificate"` } +// ConfigOAuthBitbucketServer describes the Bitbucket integration configuration for a Coder Enterprise deployment. type ConfigOAuthBitbucketServer struct { BaseURL string `json:"base_url" diff:"oauth.bitbucket_server.base_url"` } +// ConfigOAuthGitHub describes the Github integration configuration for a Coder Enterprise deployment. type ConfigOAuthGitHub struct { BaseURL string `json:"base_url"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` } +// ConfigOAuthGitLab describes the GitLab integration configuration for a Coder Enterprise deployment. type ConfigOAuthGitLab struct { BaseURL string `json:"base_url"` ClientID string `json:"client_id" ` ClientSecret string `json:"client_secret"` } +// ConfigOAuth describes the aggregate git integration configuration for a Coder Enterprise deployment. type ConfigOAuth struct { BitbucketServer ConfigOAuthBitbucketServer `json:"bitbucket_server"` GitHub ConfigOAuthGitHub `json:"github"` GitLab ConfigOAuthGitLab `json:"gitlab"` } +// SiteConfigAuth fetches the sitewide authentication configuration. func (c Client) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { var conf ConfigAuth if err := c.requestBody(ctx, http.MethodGet, "/api/auth/config", nil, &conf); err != nil { @@ -64,10 +73,12 @@ func (c Client) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { return &conf, nil } +// PutSiteConfigAuth sets the sitewide authentication configuration. func (c Client) PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error { return c.requestBody(ctx, http.MethodPut, "/api/auth/config", req, nil) } +// SiteConfigOAuth fetches the sitewide git provider OAuth configuration. func (c Client) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { var conf ConfigOAuth if err := c.requestBody(ctx, http.MethodGet, "/api/oauth/config", nil, &conf); err != nil { @@ -76,6 +87,7 @@ func (c Client) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { return &conf, nil } +// PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. func (c Client) PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error { return c.requestBody(ctx, http.MethodPut, "/api/oauth/config", req, nil) } @@ -84,6 +96,7 @@ type configSetupMode struct { SetupMode bool `json:"setup_mode"` } +// SiteSetupModeEnabled fetches the current setup_mode state of a Coder Enterprise deployment. func (c Client) SiteSetupModeEnabled(ctx context.Context) (bool, error) { var conf configSetupMode if err := c.requestBody(ctx, http.MethodGet, "/api/config/setup-mode", nil, &conf); err != nil { @@ -92,6 +105,7 @@ func (c Client) SiteSetupModeEnabled(ctx context.Context) (bool, error) { return conf.SetupMode, nil } +// ExtensionMarketplaceType is an enum of the valid extension marketplace configurations. type ExtensionMarketplaceType string // ExtensionMarketplaceType enum. @@ -101,13 +115,16 @@ const ( ExtensionMarketplacePublic ExtensionMarketplaceType = "public" ) +// MarketplaceExtensionPublicURL is the URL of the coder.com public marketplace that serves open source Code OSS extensions. const MarketplaceExtensionPublicURL = "https://extensions.coder.com/api" +// ConfigExtensionMarketplace describes the sitewide extension marketplace configuration. type ConfigExtensionMarketplace struct { URL string `json:"url"` Type ExtensionMarketplaceType `json:"type"` } +// SiteConfigExtensionMarketplace fetches the extension marketplace configuration. func (c Client) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) { var conf ConfigExtensionMarketplace if err := c.requestBody(ctx, http.MethodGet, "/api/extensions/config", nil, &conf); err != nil { @@ -116,6 +133,7 @@ func (c Client) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExte return &conf, nil } +// PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. func (c Client) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { return c.requestBody(ctx, http.MethodPut, "/api/extensions/config", req, nil) } diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 42bb6467..cf4cfcc9 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -10,7 +10,7 @@ import ( "nhooyr.io/websocket/wsjson" ) -// Environment describes a Coder environment +// Environment describes a Coder environment. type Environment struct { ID string `json:"id" table:"-"` Name string `json:"name" table:"Name"` @@ -40,7 +40,7 @@ type RebuildMessage struct { AutoOffThreshold Duration `json:"auto_off_threshold"` } -// EnvironmentStat represents the state of an environment +// EnvironmentStat represents the state of an environment. type EnvironmentStat struct { Time time.Time `json:"time"` LastOnline time.Time `json:"last_online"` @@ -58,7 +58,7 @@ func (e EnvironmentStat) String() string { return string(e.ContainerStatus) } // EnvironmentStatus refers to the states of an environment. type EnvironmentStatus string -// The following represent the possible environment container states +// The following represent the possible environment container states. const ( EnvironmentCreating EnvironmentStatus = "CREATING" EnvironmentOff EnvironmentStatus = "OFF" @@ -89,7 +89,7 @@ func (c Client) CreateEnvironment(ctx context.Context, orgID string, req CreateE } // Environments lists environments returned by the given filter. -// TODO: add the filter options, explore performance issues +// TODO: add the filter options, explore performance issue. func (c Client) Environments(ctx context.Context) ([]Environment, error) { var envs []Environment if err := c.requestBody(ctx, http.MethodGet, "/api/environments", nil, &envs); err != nil { @@ -146,7 +146,7 @@ func (c Client) DialWsep(ctx context.Context, env *Environment) (*websocket.Conn return c.dialWebsocket(ctx, "/proxy/environments/"+env.ID+"/wsep") } -// DialIDEStatus opens a websocket connection for cpu load metrics on the environment +// DialIDEStatus opens a websocket connection for cpu load metrics on the environment. func (c Client) DialIDEStatus(ctx context.Context, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status") } @@ -204,7 +204,7 @@ func (c Client) DialEnvironmentStats(ctx context.Context, envID string) (*websoc return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-stats") } -// DialResourceLoad opens a websocket connection for cpu load metrics on the environment +// DialResourceLoad opens a websocket connection for cpu load metrics on the environment. func (c Client) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-resource-load") } @@ -233,7 +233,7 @@ type buildLogMsg struct { Type BuildLogType `json:"type"` } -// WaitForEnvironmentReady will watch the build log and return when done +// WaitForEnvironmentReady will watch the build log and return when done. func (c Client) WaitForEnvironmentReady(ctx context.Context, env *Environment) error { conn, err := c.DialEnvironmentBuildLog(ctx, env.ID) if err != nil { diff --git a/coder-sdk/error.go b/coder-sdk/error.go index 75667ef1..e5d16eb2 100644 --- a/coder-sdk/error.go +++ b/coder-sdk/error.go @@ -9,11 +9,13 @@ import ( "golang.org/x/xerrors" ) -// ErrNotFound describes an error case in which the requested resource could not be found +// ErrNotFound describes an error case in which the requested resource could not be found. var ErrNotFound = xerrors.New("resource not found") +// ErrPermissions describes an error case in which the requester has insufficient permissions to access the requested resource. var ErrPermissions = xerrors.New("insufficient permissions") +// ErrAuthentication describes the error case in which the requester has invalid authentication. var ErrAuthentication = xerrors.New("invalid authentication") // APIError is the expected payload format for our errors. diff --git a/coder-sdk/image.go b/coder-sdk/image.go index edeecabf..f87e9384 100644 --- a/coder-sdk/image.go +++ b/coder-sdk/image.go @@ -5,7 +5,7 @@ import ( "net/http" ) -// Image describes a Coder Image +// Image describes a Coder Image. type Image struct { ID string `json:"id"` OrganizationID string `json:"organization_id"` @@ -18,7 +18,7 @@ type Image struct { Deprecated bool `json:"deprecated"` } -// NewRegistryRequest describes a docker registry used in importing an image +// NewRegistryRequest describes a docker registry used in importing an image. type NewRegistryRequest struct { FriendlyName string `json:"friendly_name"` Registry string `json:"registry"` @@ -26,7 +26,7 @@ type NewRegistryRequest struct { Password string `json:"password"` } -// ImportImageReq is used to import new images and registries into Coder +// ImportImageReq is used to import new images and registries into Coder. type ImportImageReq struct { RegistryID *string `json:"registry_id"` // Used to import images to existing registries. NewRegistry *NewRegistryRequest `json:"new_registry"` // Used when adding a new registry. @@ -39,7 +39,7 @@ type ImportImageReq struct { URL string `json:"url"` } -// ImportImage creates a new image and optionally a new registry +// ImportImage creates a new image and optionally a new registry. func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageReq) (*Image, error) { var img Image if err := c.requestBody(ctx, http.MethodPost, "/api/orgs/"+orgID+"/images", req, &img); err != nil { diff --git a/coder-sdk/org.go b/coder-sdk/org.go index 0caa9d18..d42235ae 100644 --- a/coder-sdk/org.go +++ b/coder-sdk/org.go @@ -6,28 +6,28 @@ import ( "time" ) -// Organization describes an Organization in Coder +// Organization describes an Organization in Coder. type Organization struct { ID string `json:"id"` Name string `json:"name"` Members []OrganizationUser `json:"members"` } -// OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization +// OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization. type OrganizationUser struct { User OrganizationRoles []Role `json:"organization_roles"` RolesUpdatedAt time.Time `json:"roles_updated_at"` } -// Organization Roles +// Organization Roles. const ( RoleOrgMember Role = "organization-member" RoleOrgAdmin Role = "organization-admin" RoleOrgManager Role = "organization-manager" ) -// Organizations gets all Organizations +// Organizations gets all Organizations. func (c Client) Organizations(ctx context.Context) ([]Organization, error) { var orgs []Organization if err := c.requestBody(ctx, http.MethodGet, "/api/orgs", nil, &orgs); err != nil { @@ -36,6 +36,7 @@ func (c Client) Organizations(ctx context.Context) ([]Organization, error) { return orgs, nil } +// OrganizationByID get the Organization by its ID. func (c Client) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) { var org Organization err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID, nil, &org) @@ -45,7 +46,7 @@ func (c Client) OrganizationByID(ctx context.Context, orgID string) (*Organizati return &org, nil } -// OrganizationMembers get all members of the given organization +// OrganizationMembers get all members of the given organization. func (c Client) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { var members []OrganizationUser if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/members", nil, &members); err != nil { @@ -54,6 +55,7 @@ func (c Client) OrganizationMembers(ctx context.Context, orgID string) ([]Organi return members, nil } +// UpdateOrganizationReq describes the patch request parameters to provide partial updates to an Organization resource. type UpdateOrganizationReq struct { Name *string `json:"name"` Description *string `json:"description"` @@ -63,10 +65,12 @@ type UpdateOrganizationReq struct { MemoryProvisioningRate *float32 `json:"memory_provisioning_rate"` } +// UpdateOrganization applys a partial update of an Organization resource. func (c Client) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/orgs/"+orgID, req, nil) } +// CreateOrganizationReq describes the request parameters to create a new Organization. type CreateOrganizationReq struct { Name string `json:"name"` Description string `json:"description"` @@ -77,10 +81,12 @@ type CreateOrganizationReq struct { MemoryProvisioningRate float32 `json:"memory_provisioning_rate"` } +// CreateOrganization creates a new Organization in Coder Enterprise. func (c Client) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error { return c.requestBody(ctx, http.MethodPost, "/api/orgs", req, nil) } +// DeleteOrganization deletes an organization. func (c Client) DeleteOrganization(ctx context.Context, orgID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/orgs/"+orgID, nil, nil) } diff --git a/coder-sdk/secrets.go b/coder-sdk/secrets.go index d4dfa1eb..9043091d 100644 --- a/coder-sdk/secrets.go +++ b/coder-sdk/secrets.go @@ -6,7 +6,7 @@ import ( "time" ) -// Secret describes a Coder secret +// Secret describes a Coder secret. type Secret struct { ID string `json:"id" table:"-"` Name string `json:"name" table:"Name"` @@ -16,7 +16,7 @@ type Secret struct { UpdatedAt time.Time `json:"updated_at" table:"-"` } -// Secrets gets all secrets for the given user +// Secrets gets all secrets for the given user. func (c *Client) Secrets(ctx context.Context, userID string) ([]Secret, error) { var secrets []Secret if err := c.requestBody(ctx, http.MethodGet, "/api/users/"+userID+"/secrets", nil, &secrets); err != nil { @@ -51,7 +51,7 @@ func (c *Client) SecretWithValueByID(ctx context.Context, id, userID string) (*S return &secret, nil } -// SecretByName gets a secret object by name +// SecretByName gets a secret object by name. func (c *Client) SecretByName(ctx context.Context, name, userID string) (*Secret, error) { secrets, err := c.Secrets(ctx, userID) if err != nil { @@ -72,12 +72,12 @@ type InsertSecretReq struct { Description string `json:"description"` } -// InsertSecret adds a new secret for the authed user +// InsertSecret adds a new secret for the authed user. func (c *Client) InsertSecret(ctx context.Context, user *User, req InsertSecretReq) error { return c.requestBody(ctx, http.MethodPost, "/api/users/"+user.ID+"/secrets", req, nil) } -// DeleteSecretByName deletes the authenticated users secret with the given name +// DeleteSecretByName deletes the authenticated users secret with the given name. func (c *Client) DeleteSecretByName(ctx context.Context, name, userID string) error { // Lookup the secret by name to get the ID. secret, err := c.SecretByName(ctx, name, userID) diff --git a/coder-sdk/tokens.go b/coder-sdk/tokens.go index 946979c0..adb021a1 100644 --- a/coder-sdk/tokens.go +++ b/coder-sdk/tokens.go @@ -6,6 +6,7 @@ import ( "time" ) +// APIToken describes a Coder Enterprise APIToken resource for use in API requests. type APIToken struct { ID string `json:"id"` Name string `json:"name"` @@ -14,6 +15,7 @@ type APIToken struct { LastUsed time.Time `json:"last_used"` } +// CreateAPITokenReq defines the paramemters for creating a new APIToken. type CreateAPITokenReq struct { Name string `json:"name"` } @@ -22,6 +24,7 @@ type createAPITokenResp struct { Key string `json:"key"` } +// CreateAPIToken creates a new APIToken for making authenticated requests to Coder Enterprise. func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) { var resp createAPITokenResp err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID, req, &resp) @@ -31,6 +34,7 @@ func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPI return resp.Key, nil } +// APITokens fetches all APITokens owned by the given user. func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error) { var tokens []APIToken if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID, nil, &tokens); err != nil { @@ -39,6 +43,7 @@ func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error return tokens, nil } +// APITokenByID fetches the metadata for a given APIToken. func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) { var token APIToken if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil { @@ -47,10 +52,12 @@ func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIT return &token, nil } +// DeleteAPIToken deletes an APIToken. func (c Client) DeleteAPIToken(ctx context.Context, userID, tokenID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/api-keys/"+userID+"/"+tokenID, nil, nil) } +// RegenerateAPIToken regenerates the given APIToken and returns the new value. func (c Client) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) { var resp createAPITokenResp if err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil { diff --git a/coder-sdk/users.go b/coder-sdk/users.go index 59c3a6f1..232e3a23 100644 --- a/coder-sdk/users.go +++ b/coder-sdk/users.go @@ -20,11 +20,10 @@ type User struct { UpdatedAt time.Time `json:"updated_at" table:"-"` } +// Role defines a Coder Enterprise permissions role group. type Role string -type Roles []Role - -// Site Roles +// Site Roles. const ( SiteAdmin Role = "site-admin" SiteAuditor Role = "site-auditor" @@ -32,8 +31,10 @@ const ( SiteMember Role = "site-member" ) +// LoginType defines the enum of valid user login types. type LoginType string +// LoginType enum options. const ( LoginTypeBuiltIn LoginType = "built-in" LoginTypeSAML LoginType = "saml" @@ -100,7 +101,7 @@ func (c Client) UserByEmail(ctx context.Context, email string) (*User, error) { type UpdateUserReq struct { // TODO(@cmoog) add update password option Revoked *bool `json:"revoked,omitempty"` - Roles *Roles `json:"roles,omitempty"` + Roles *[]Role `json:"roles,omitempty"` LoginType *LoginType `json:"login_type,omitempty"` Name *string `json:"name,omitempty"` Username *string `json:"username,omitempty"` @@ -108,10 +109,12 @@ type UpdateUserReq struct { DotfilesGitURL *string `json:"dotfiles_git_uri,omitempty"` } +// UpdateUser applyes the partial update to the given user. func (c Client) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/users/"+userID, req, nil) } +// CreateUserReq defines the request parameters for creating a new user resource. type CreateUserReq struct { Name string `json:"name"` Username string `json:"username"` @@ -122,10 +125,12 @@ type CreateUserReq struct { OrganizationsIDs []string `json:"organizations"` } +// CreateUser creates a new user account. func (c Client) CreateUser(ctx context.Context, req CreateUserReq) error { return c.requestBody(ctx, http.MethodPost, "/api/users", req, nil) } +// DeleteUser deletes a user account. func (c Client) DeleteUser(ctx context.Context, userID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/users/"+userID, nil, nil) } diff --git a/coder-sdk/util.go b/coder-sdk/util.go index 882b3ced..0abba3c9 100644 --- a/coder-sdk/util.go +++ b/coder-sdk/util.go @@ -6,6 +6,7 @@ import ( "time" ) +// String gives a string pointer. func String(s string) *string { return &s } diff --git a/pkg/tcli/tcli.go b/pkg/tcli/tcli.go index 51ec6f55..596dda54 100644 --- a/pkg/tcli/tcli.go +++ b/pkg/tcli/tcli.go @@ -29,7 +29,7 @@ type runnable interface { io.Closer } -// ContainerConfig describes the ContainerRunner configuration schema for initializing a testing environment +// ContainerConfig describes the ContainerRunner configuration schema for initializing a testing environment. type ContainerConfig struct { Name string Image string @@ -51,13 +51,13 @@ func preflightChecks() error { return nil } -// ContainerRunner specifies a runtime container for performing command tests +// ContainerRunner specifies a runtime container for performing command tests. type ContainerRunner struct { name string ctx context.Context } -// NewContainerRunner starts a new docker container for executing command tests +// NewContainerRunner starts a new docker container for executing command tests. func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*ContainerRunner, error) { if err := preflightChecks(); err != nil { return nil, err @@ -87,7 +87,7 @@ func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*Containe }, nil } -// Close kills and removes the command execution testing container +// Close kills and removes the command execution testing container. func (r *ContainerRunner) Close() error { cmd := exec.CommandContext(r.ctx, "sh", "-c", strings.Join([]string{ @@ -118,7 +118,7 @@ func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { } } -// RunCmd lifts the given *exec.Cmd into the runtime container +// RunCmd lifts the given *exec.Cmd into the runtime container. func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { path, _ := exec.LookPath("docker") cmd.Path = path @@ -131,7 +131,7 @@ func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { } } -// HostRunner executes command tests on the host, outside of a container +// HostRunner executes command tests on the host, outside of a container. type HostRunner struct{} // Run executes the given command on the host. @@ -145,7 +145,7 @@ func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { } } -// RunCmd executes the given *exec.Cmd on the host +// RunCmd executes the given *exec.Cmd on the host. func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { return &Assertable{ cmd: cmd, @@ -153,18 +153,18 @@ func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { } } -// Close is a noop for HostRunner +// Close is a noop for HostRunner. func (r *HostRunner) Close() error { return nil } -// Assertable describes an initialized command ready to be run and asserted against +// Assertable describes an initialized command ready to be run and asserted against. type Assertable struct { cmd *exec.Cmd tname string } -// Assert runs the Assertable and +// Assert runs the Assertable and. func (a *Assertable) Assert(t *testing.T, option ...Assertion) { slog.Helper() var ( @@ -183,10 +183,12 @@ func (a *Assertable) Assert(t *testing.T, option ...Assertion) { err := a.cmd.Run() result.Duration = time.Since(start) - if exitErr, ok := err.(*exec.ExitError); ok { - result.ExitCode = exitErr.ExitCode() - } else if err != nil { - slogtest.Fatal(t, "command failed to run", slog.Error(err), slog.F("command", a.cmd)) + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + result.ExitCode = exitErr.ExitCode() + } else { + slogtest.Fatal(t, "command failed to run", slog.Error(err), slog.F("command", a.cmd)) + } } else { result.ExitCode = 0 } @@ -211,20 +213,20 @@ func (a *Assertable) Assert(t *testing.T, option ...Assertion) { // Pass custom Assertion functions to cover special cases. type Assertion func(t *testing.T, r *CommandResult) -// CommandResult contains the aggregated result of a command execution +// CommandResult contains the aggregated result of a command execution. type CommandResult struct { Stdout, Stderr []byte ExitCode int Duration time.Duration } -// Success asserts that the command exited with an exit code of 0 +// Success asserts that the command exited with an exit code of 0. func Success() Assertion { slog.Helper() return ExitCodeIs(0) } -// Error asserts that the command exited with a nonzero exit code +// Error asserts that the command exited with a nonzero exit code. func Error() Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -232,7 +234,7 @@ func Error() Assertion { } } -// ExitCodeIs asserts that the command exited with the given code +// ExitCodeIs asserts that the command exited with the given code. func ExitCodeIs(code int) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -240,7 +242,7 @@ func ExitCodeIs(code int) Assertion { } } -// StdoutEmpty asserts that the command did not write any data to Stdout +// StdoutEmpty asserts that the command did not write any data to Stdout. func StdoutEmpty() Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -249,7 +251,7 @@ func StdoutEmpty() Assertion { } // GetResult offers an escape hatch from tcli -// The pointer passed as "result" will be assigned to the command's *CommandResult +// The pointer passed as "result" will be assigned to the command's *CommandResult. func GetResult(result **CommandResult) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -257,7 +259,7 @@ func GetResult(result **CommandResult) Assertion { } } -// StderrEmpty asserts that the command did not write any data to Stderr +// StderrEmpty asserts that the command did not write any data to Stderr. func StderrEmpty() Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -265,7 +267,7 @@ func StderrEmpty() Assertion { } } -// StdoutMatches asserts that Stdout contains a substring which matches the given regexp +// StdoutMatches asserts that Stdout contains a substring which matches the given regexp. func StdoutMatches(pattern string) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -273,7 +275,7 @@ func StdoutMatches(pattern string) Assertion { } } -// StderrMatches asserts that Stderr contains a substring which matches the given regexp +// StderrMatches asserts that Stderr contains a substring which matches the given regexp. func StderrMatches(pattern string) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -305,7 +307,7 @@ func empty(t *testing.T, name string, a []byte) { } } -// DurationLessThan asserts that the command completed in less than the given duration +// DurationLessThan asserts that the command completed in less than the given duration. func DurationLessThan(dur time.Duration) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -318,7 +320,7 @@ func DurationLessThan(dur time.Duration) Assertion { } } -// DurationGreaterThan asserts that the command completed in greater than the given duration +// DurationGreaterThan asserts that the command completed in greater than the given duration. func DurationGreaterThan(dur time.Duration) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -331,7 +333,7 @@ func DurationGreaterThan(dur time.Duration) Assertion { } } -// StdoutJSONUnmarshal attempts to unmarshal stdout into the given target +// StdoutJSONUnmarshal attempts to unmarshal stdout into the given target. func StdoutJSONUnmarshal(target interface{}) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() @@ -340,7 +342,7 @@ func StdoutJSONUnmarshal(target interface{}) Assertion { } } -// StderrJSONUnmarshal attempts to unmarshal stderr into the given target +// StderrJSONUnmarshal attempts to unmarshal stderr into the given target. func StderrJSONUnmarshal(target interface{}) Assertion { return func(t *testing.T, r *CommandResult) { slog.Helper() From 5ad33969b0eec804008009d039166837bfe8ee54 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 20:23:51 -0600 Subject: [PATCH 3/6] fixup! Enable more linting rules --- ci/integration/setup_test.go | 8 ++++---- cmd/coder/main.go | 8 +++++--- internal/activity/writer.go | 3 ++- internal/cmd/auth.go | 3 +-- internal/cmd/cmd.go | 5 +++-- internal/cmd/rebuild.go | 2 +- internal/cmd/resourcemanager.go | 4 ++-- internal/cmd/secrets.go | 7 ++++--- internal/cmd/shell.go | 4 ++-- internal/cmd/urls.go | 2 +- internal/loginsrv/input.go | 2 ++ internal/sync/eventcache.go | 3 +-- internal/sync/sync.go | 13 +++++++------ internal/version/version.go | 4 +++- internal/x/xterminal/terminal.go | 2 +- 15 files changed, 39 insertions(+), 31 deletions(-) diff --git a/ci/integration/setup_test.go b/ci/integration/setup_test.go index 1ccb8e0e..cce4b440 100644 --- a/ci/integration/setup_test.go +++ b/ci/integration/setup_test.go @@ -13,10 +13,10 @@ import ( "golang.org/x/xerrors" ) -// binpath is populated during package initialization with a path to the coder binary +// binpath is populated during package initialization with a path to the coder binary. var binpath string -// initialize integration tests by building the coder-cli binary +// initialize integration tests by building the coder-cli binary. func init() { cwd, err := os.Getwd() if err != nil { @@ -30,7 +30,7 @@ func init() { } } -// build the coder-cli binary and move to the integration testing bin directory +// build the coder-cli binary and move to the integration testing bin directory. func build(path string) error { tar := "coder-cli-linux-amd64.tar.gz" dir := filepath.Dir(path) @@ -48,7 +48,7 @@ func build(path string) error { return nil } -// write session tokens to the given container runner +// write session tokens to the given container runner. func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { creds := login(ctx, t) cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p $HOME/.config/coder && cat > $HOME/.config/coder/session") diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 73e1858d..b491dfa8 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -17,7 +17,6 @@ import ( func main() { ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // If requested, spin up the pprof webserver. if os.Getenv("PPROF") != "" { @@ -31,16 +30,19 @@ func main() { clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) os.Exit(1) } - defer func() { + restoreTerminal := func() { // Best effort. Would result in broken terminal on window but nothing we can do about it. _ = xterminal.Restore(os.Stdout.Fd(), stdoutState) - }() + } app := cmd.Make() app.Version = fmt.Sprintf("%s %s %s/%s", version.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) if err := app.ExecuteContext(ctx); err != nil { clog.Log(err) + restoreTerminal() os.Exit(1) } + restoreTerminal() + cancel() } diff --git a/internal/activity/writer.go b/internal/activity/writer.go index 0daf6dc8..02d9d1b8 100644 --- a/internal/activity/writer.go +++ b/internal/activity/writer.go @@ -1,3 +1,4 @@ +// Package activity defines the logic for tracking usage activity metrics. package activity import ( @@ -17,7 +18,7 @@ func (w *writer) Write(buf []byte) (int, error) { return w.wr.Write(buf) } -// Writer wraps the given writer such that all writes trigger an activity push +// Writer wraps the given writer such that all writes trigger an activity push. func (p *Pusher) Writer(wr io.Writer) io.Writer { return &writer{p: p, wr: wr} } diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 49fb4e0d..aa204d1e 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -56,8 +56,7 @@ func newClient(ctx context.Context) (*coder.Client, error) { if err != nil { var he *coder.HTTPError if xerrors.As(err, &he) { - switch he.StatusCode { - case http.StatusUnauthorized: + if he.StatusCode == http.StatusUnauthorized { return nil, xerrors.Errorf("not authenticated: try running \"coder login`\"") } } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 462c7c3a..937c0558 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -1,3 +1,4 @@ +// Package cmd constructs all subcommands for coder-cli. package cmd import ( @@ -7,10 +8,10 @@ import ( "github.com/spf13/cobra/doc" ) -// verbose is a global flag for specifying that a command should give verbose output +// verbose is a global flag for specifying that a command should give verbose output. var verbose bool = false -// Make constructs the "coder" root command +// Make constructs the "coder" root command. func Make() *cobra.Command { app := &cobra.Command{ Use: "coder", diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index ebb1cc8a..ae2907d3 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -73,7 +73,7 @@ coder envs rebuild backend-env --force`, } // trailBuildLogs follows the build log for a given environment and prints the staged -// output with loaders and success/failure indicators for each stage +// output with loaders and success/failure indicators for each stage. func trailBuildLogs(ctx context.Context, client *coder.Client, envID string) error { const check = "✅" const failure = "❌" diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 2e636d7e..df411e00 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -143,7 +143,7 @@ func aggregateByOrg(users []coder.User, orgs []coder.Organization, envs []coder. return groups, userLabeler{userIDMap} } -// groupable specifies a structure capable of being an aggregation group of environments (user, org, all) +// groupable specifies a structure capable of being an aggregation group of environments (user, org, all). type groupable interface { header() string environments() []coder.Environment @@ -334,7 +334,7 @@ func (a resources) memUtilPercentage() string { return fmt.Sprintf("%.1f%%", a.memUtilization/a.memAllocation*100) } -// truncate the given string and replace the removed chars with some replacement (ex: "...") +// truncate the given string and replace the removed chars with some replacement (ex: "..."). func truncate(str string, max int, replace string) string { if len(str) <= max { return str diff --git a/internal/cmd/secrets.go b/internal/cmd/secrets.go index 9fedb6a1..41e38443 100644 --- a/internal/cmd/secrets.go +++ b/internal/cmd/secrets.go @@ -98,15 +98,16 @@ coder secrets create aws-credentials --from-file ./credentials.json`, if err != nil { return err } - if fromLiteral != "" { + switch { + case fromLiteral != "": value = fromLiteral - } else if fromFile != "" { + case fromFile != "": contents, err := ioutil.ReadFile(fromFile) if err != nil { return xerrors.Errorf("read file: %w", err) } value = string(contents) - } else { + default: prompt := promptui.Prompt{ Label: "value", Mask: '*', diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go index c4466870..acf733df 100644 --- a/internal/cmd/shell.go +++ b/internal/cmd/shell.go @@ -222,16 +222,16 @@ func networkErr(env *coder.Environment) error { func heartbeat(ctx context.Context, conn *websocket.Conn, interval time.Duration) { ticker := time.NewTicker(interval) - defer ticker.Stop() - for { select { case <-ctx.Done(): + ticker.Stop() return case <-ticker.C: if err := conn.Ping(ctx); err != nil { // don't try to do multi-line here because the raw mode makes things weird clog.Log(clog.Fatal("failed to ping websocket, exiting: " + err.Error())) + ticker.Stop() os.Exit(1) } } diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index a856832c..26e5d5bd 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -49,7 +49,7 @@ func urlCmd() *cobra.Command { return cmd } -// DevURL is the parsed json response record for a devURL from cemanager +// DevURL is the parsed json response record for a devURL from cemanager. type DevURL struct { ID string `json:"id" table:"-"` URL string `json:"url" table:"URL"` diff --git a/internal/loginsrv/input.go b/internal/loginsrv/input.go index 99de2015..cc0ae255 100644 --- a/internal/loginsrv/input.go +++ b/internal/loginsrv/input.go @@ -1,3 +1,5 @@ +// Package loginsrv defines the login server in use by coder-cli +// for performing the browser authentication flow. package loginsrv import ( diff --git a/internal/sync/eventcache.go b/internal/sync/eventcache.go index b4924da1..1073b123 100644 --- a/internal/sync/eventcache.go +++ b/internal/sync/eventcache.go @@ -17,9 +17,8 @@ type eventCache map[string]timedEvent func (cache eventCache) Add(ev timedEvent) { lastEvent, ok := cache[ev.Path()] if ok { - switch { // If the file was quickly created and then destroyed, pretend nothing ever happened. - case lastEvent.Event() == notify.Create && ev.Event() == notify.Remove: + if lastEvent.Event() == notify.Create && ev.Event() == notify.Remove { delete(cache, ev.Path()) return } diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 225ec598..ee990c1b 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -1,3 +1,4 @@ +// Package sync contains logic for establishing a file sync between a local machine and a Coder Enterprise environment. package sync import ( @@ -74,13 +75,13 @@ func (s Sync) syncPaths(delete bool, local, remote string) error { if err := cmd.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { - if exitError.ExitCode() == rsyncExitCodeIncompat { + switch { + case exitError.ExitCode() == rsyncExitCodeIncompat: return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err) - } else if exitError.ExitCode() == rsyncExitCodeDataStream { + case exitError.ExitCode() == rsyncExitCodeDataStream: return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err) - } else { - return xerrors.Errorf("rsync: %w", err) } + return xerrors.Errorf("rsync: %w", err) } return xerrors.Errorf("rsync: %w", err) } @@ -207,7 +208,7 @@ func (s Sync) work(ev timedEvent) { } } -// ErrRestartSync describes a known error case that can be solved by re-starting the command +// ErrRestartSync describes a known error case that can be solved by re-starting the command. var ErrRestartSync = errors.New("the sync exited because it was overloaded, restart it") // workEventGroup converges a group of events to prevent duplicate work. @@ -302,7 +303,7 @@ func (s Sync) Version() (string, error) { // Run starts the sync synchronously. // Use this command to debug what wasn't sync'd correctly: -// rsync -e "coder sh" -nicr ~/Projects/cdr/coder-cli/. ammar:/home/coder/coder-cli/ +// rsync -e "coder sh" -nicr ~/Projects/cdr/coder-cli/. ammar:/home/coder/coder-cli/. func (s Sync) Run() error { events := make(chan notify.EventInfo, maxInflightInotify) // Set up a recursive watch. diff --git a/internal/version/version.go b/internal/version/version.go index 88b0626c..ce1d5de9 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,3 +1,5 @@ +// Package version contains the compile-time injected version string and +// related utiliy methods. package version import ( @@ -7,7 +9,7 @@ import ( // Version is populated at compile-time with the current coder-cli version. var Version string = "unknown" -// VersionMatch compares the given APIVersion to the compile-time injected coder-cli version. +// VersionsMatch compares the given APIVersion to the compile-time injected coder-cli version. func VersionsMatch(apiVersion string) bool { withoutPatchRelease := strings.Split(Version, ".") if len(withoutPatchRelease) < 3 { diff --git a/internal/x/xterminal/terminal.go b/internal/x/xterminal/terminal.go index 374869d8..dd2543e8 100644 --- a/internal/x/xterminal/terminal.go +++ b/internal/x/xterminal/terminal.go @@ -42,7 +42,7 @@ func ColorEnabled(fd uintptr) (bool, error) { return terminal.IsTerminal(int(fd)), nil } -// ResizeEvent describes the new terminal dimensions following a resize +// ResizeEvent describes the new terminal dimensions following a resize. type ResizeEvent struct { Height uint16 Width uint16 From f499d6c34a8a92ce96821c960b96cd4ff07d6cea Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 20:26:17 -0600 Subject: [PATCH 4/6] fixup! Enable more linting rules --- internal/cmd/ceapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index 3d91c176..1e7e2480 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -58,7 +58,7 @@ func getEnvs(ctx context.Context, client *coder.Client, email string) ([]coder.E return allEnvs, nil } -// findEnv returns a single environment by name (if it exists.) +// findEnv returns a single environment by name (if it exists.). func findEnv(ctx context.Context, client *coder.Client, envName, userEmail string) (*coder.Environment, error) { envs, err := getEnvs(ctx, client, userEmail) if err != nil { From 40006ff4bb82d070b25d01724ca53efefe8fbaaf Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 20:29:37 -0600 Subject: [PATCH 5/6] fixup! Enable more linting rules --- cmd/coder/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index b491dfa8..7a7f2716 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -28,6 +28,7 @@ func main() { stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) if err != nil { clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) + cancel() os.Exit(1) } restoreTerminal := func() { @@ -40,6 +41,7 @@ func main() { if err := app.ExecuteContext(ctx); err != nil { clog.Log(err) + cancel() restoreTerminal() os.Exit(1) } From eed820b036cf2cf62429a7a12296b165b97bca78 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 3 Nov 2020 20:29:54 -0600 Subject: [PATCH 6/6] fixup! Enable more linting rules --- cmd/coder/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 7a7f2716..51ce8614 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -45,6 +45,6 @@ func main() { restoreTerminal() os.Exit(1) } - restoreTerminal() cancel() + restoreTerminal() } pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy