Help us shape the future of Go! Take the 2026 Go Developer Survey

gitlab

package
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package gitlab provides a wrapper around the GitLab REST API v4 client. Some domains additionally use the GitLab GraphQL API for endpoints not covered by client-go service wrappers (see ADR-0006).

The package also detects Personal Access Token scopes so HTTP server entries can register tools according to the authenticated token's capabilities.

Resilience

Client initialization is lazy and recoverable. When GitLab is temporarily unavailable at startup, the server can enter degraded mode and retry initialization on later API calls with rate-limited health checks. This keeps local MCP startup responsive while still surfacing actionable errors to tool handlers.

Edition and Scope Detection

The wrapper tracks GitLab.com and Premium/Ultimate detection separately from token-scope detection. HTTP mode uses that information to register the correct tool catalog for each token and GitLab URL pair.

Index

Constants

View Source
const AllowPrivateInstancesEnv = config.EnvPrefix + "ALLOW_PRIVATE_INSTANCES"

AllowPrivateInstancesEnv names the setting that permits a destination a caller or a redirect chose to resolve to a private, loopback, CGNAT, link-local, unique-local or unspecified address.

It is the opt-out for tier B and only tier B: the cloud metadata addresses are refused whatever it says, because no value of this setting is a claim that a GitLab API or a presigned object store is served from one.

The `--allow-private-instances` flag writes this variable, so there stays one reader; see cmd/server/env_flags.go for why that indirection exists.

View Source
const DefaultMaxResponseBytes int64 = 64 << 20

DefaultMaxResponseBytes is how much of one GitLab response body a client will read before giving up on it.

The number bounds the *decompressed* stream, which is the only place it can do any good: net/http gunzips a Content-Encoding: gzip response inside the base transport, so a cap applied to wire bytes bounds the compressed size and misses the amplification entirely. A 528 KB gzip that inflates to 67 MB is an ordinary artifact of a hostile or compromised upstream, and the SDK decodes whatever it is handed with no ceiling of its own.

64 MiB is chosen to sit above every response this server asks for in practice — the largest are job traces, which the jobs package truncates to 100 KB of its own accord, and artifact and export downloads, which are base64-encoded into a JSON-RPC response and are already unusable long before this — while being far below the multi-gigabyte working set an unbounded decode reaches. Raise it with Client.SetMaxResponseBytes if a deployment genuinely streams more than that through a tool.

View Source
const DestinationRefusedHint = allowPrivateAdvice + ". Cloud metadata addresses stay refused whatever it is set to"

DestinationRefusedHint is the next step to offer a model or an operator who has just been told a destination was refused.

It names the flag rather than describing the policy: the policy is already in the error, and what the reader does not have is the one string that changes it. Tier A has no such string, which the sentence says out loud rather than leaving a reader to try the flag and find it did nothing.

View Source
const GitLabDotComHost = "gitlab.com"

GitLabDotComHost is the canonical host for GitLab SaaS-only features.

View Source
const ScopeAPI = "api"

ScopeAPI is the GitLab scope that permits writes.

Only the write scope is named here, and deliberately: this package answers one question — can this token mutate GitLab — and the read scope is not part of that answer. internal/oauth owns the full scope vocabulary for the authorization layer; duplicating it here would be a second place to keep in step with GitLab.

Variables

View Source
var ErrDestinationRefused = errors.New("outbound destination refused")

ErrDestinationRefused reports that this server declined to open a connection to an address, before any packet was sent to it.

It is a refusal by this server rather than an answer from anything upstream, which is the distinction a reader of the message needs: nothing was reached, nothing was disclosed, and the fix is configuration rather than credentials.

View Source
var ErrNoResponseCaptured = errors.New("no response was captured")

ErrNoResponseCaptured is what ResponseCapture.Decode returns when no response passed through the transport under the capture's context: the request was never made, or was made with another context.

View Source
var ErrResponseTooLarge = errors.New("gitlab response exceeded the maximum size")

ErrResponseTooLarge reports that a GitLab response body exceeded the client's size ceiling and was abandoned rather than buffered.

It surfaces from whatever was reading the body — the SDK's JSON decode, an io.Copy into a caller's writer — so a tool handler sees a failed call rather than a truncated success.

View Source
var ErrUnboundClient = errors.New("gitlab client is unbound: the handler belongs to a shared catalog and was not bound to a credential")

ErrUnboundClient is what every request through an unbound client fails with. A handler that reports it was called through the shared, unbound copy of a catalog instead of through a copy bound to a credential, which is a wiring defect and never a user error.

Functions

func CheckCallerNamedInstance

func CheckCallerNamedInstance(rawURL string) error

CheckCallerNamedInstance refuses an instance URL a caller named that this server would decline to dial anyway.

It exists to answer at the door instead of deep inside a tool call. The dialer is still the authority — this consults the same predicate and the same opt-out — and this is the friendlier half: an operator who pointed the header at their own private GitLab gets one 400 naming the flag, rather than a server that starts, admits the credential and fails every action.

It resolves nothing. A host spelled as a literal address is judged here; a host spelled as a name is left entirely to the dialer, which sees what the name resolved to and is the only check that can. Asking DNS a question per request at the door would put a resolver timeout in front of every caller and would still not be authoritative.

func DetectScopes

func DetectScopes(ctx context.Context, client *gl.Client) []string

DetectScopes queries the GitLab PAT self endpoint to retrieve the scopes of the currently authenticated token. Returns nil on failure or when the endpoint is unavailable (GitLab < 16.0), allowing graceful fallback to registering all tools.

func HTTPTransport

func HTTPTransport(skipTLSVerify bool) http.RoundTripper

HTTPTransport returns the GitLab HTTP transport configured with the same TLS policy used by authenticated GitLab clients.

A request made through it carries no destination policy, so the dialer applies tier A and nothing else, as it always has: a caller that borrows the transport chose its own destination (ADR-0022). Without skipped verification it is the permissive half of [sharedDestinationPools], which is the pool such a request belongs in: a connection it finds there leads to an address tier A allows, and one it opens is one any request routed there would have been allowed to dial as well.

func IsGitLabDotComURL

func IsGitLabDotComURL(rawURL string) bool

IsGitLabDotComURL reports whether rawURL targets the canonical GitLab.com host.

func NarrowToTokenScope

func NarrowToTokenScope(cfg *config.ServerConfig) bool

NarrowToTokenScope marks a server configuration read-only when the token it was built for cannot write, and reports whether it did. Both transports call it once the scopes are known: the HTTP pool per entry, since an entry is per token, and stdio once at startup for its single token. The catalog built from the configuration then withholds every write action and reports it as withheld by the token scope, rather than listing actions GitLab would refuse one by one with its own 403 (ADR-0018).

A configuration already read-only is left alone, so the operator's setting and the token's limit never contradict each other in the log, and unknown scopes narrow nothing, for the reason WriteCapable gives.

func RefusalMayBePermission added in v3.1.0

func RefusalMayBePermission(status int, req *http.Request, body []byte) bool

RefusalMayBePermission reports whether a refusal GitLab answered can be a valid credential refused a permission, which is the one refusal a handler's hint about a role, a license or an owner is written for. status is the answered status, req the request GitLab answered and body the answer's body, either of which may be nil.

Only a REST 401 or 403 whose body carries no RFC 6750 error code, and whose message does not refuse the account, qualifies. GitLab's API helpers unauthorized!, forbidden! and render_api_error!, which every permission refusal goes through (the 401 ones are entry 55 of docs/development/upstream-bugs.md), render {"message": ...} and nothing else. GitLab's API guard refuses the credential rather than the call in two ways, and neither qualifies. Its token and scope refusals carry a code: invalid_token, dpop_error and restricted_language_server_client_error on a 401, insufficient_scope and insufficient_granular_scope on a 403 (lib/api/api_guard.rb). An account the API will not serve (blocked, deactivated, pending approval, an internal user, the Terms of Service not accepted, the primary email unconfirmed, the password expired) it refuses through those same helpers, with a plain 403 forbidden! whose reason is one of the fixed sentences of lib/gitlab/auth/user_access_denied_reason.rb, so that answer is told apart by its message, [accountRefusalReasons]. A role or license hint after either sends the reader to fix something that is not wrong. The GraphQL endpoint answers 401 and 403 only from its own authentication and access checks, and refuses a field the caller may not see with a 200, so none of its answers qualifies.

A request carrying no token at all does qualify, and it is harmless here. The guard maps the error for a missing token to rack-oauth2's default code unauthorized, but nothing in GitLab raises that error: such a request is answered by authenticate!'s plain 401, byte for byte a permission refusal, and this server never sends a request without a token.

The answer is "may" and not "is": a token GitLab has no record of is answered through unauthorized! with the same bytes as a permission refusal, which is why the description in front of a hint names both causes. What it guarantees is the other direction. Every 401 UnauthorizedNamesCredential says names the credential carries a code or came from GraphQL, so a hint keyed on this never follows the verdict that the token itself was refused.

func ScopeSatisfied

func ScopeSatisfied(tokenScopes, requiredScopes []string) bool

ScopeSatisfied checks whether requiredScopes are all present in the detected tokenScopes. If tokenScopes is nil (detection failed or disabled), returns true (allow all). If requiredScopes is empty, returns true (no requirement).

func UnauthorizedNamesCredential added in v3.1.0

func UnauthorizedNamesCredential(req *http.Request, body []byte) bool

UnauthorizedNamesCredential reports whether a 401 GitLab answered says the credential itself was refused, as opposed to a valid credential refused a permission. req is the request GitLab answered and body the answer's body, either of which may be nil. The caller has already established that the status is 401: neither signal means anything on another status.

Two answers say so:

  • A REST body carrying the RFC 6750 code invalid_token. GitLab's API guard answers an expired, revoked or impersonation-disabled token with rack-oauth2's body carrying that code (lib/api/api_guard.rb, pinned by spec/requests/api/api_guard_spec.rb), and nothing else in the REST API writes it. GitLab's API helper unauthorized! (lib/api/helpers.rb), which the permission refusals call, renders {"message":"401 Unauthorized"} instead.
  • Any 401 from the GraphQL endpoint. It answers 401 only from its authentication checks (GraphqlController#authorize_access_api! renders {"errors":[{"message":"Invalid token"}]} for a token it could not use, and the two authentication errors the controller rescues are the others), and it refuses a field the caller may not see with a 200 carrying errors, never with a 401. So the permission refusal a REST 401 can be has no GraphQL counterpart to be confused with.

The absence of both decides nothing. A token GitLab cannot find at all is answered through unauthorized! too, byte for byte like a permission refusal (auth_finders.rb raises UnauthorizedError, which the API helpers turn into unauthorized!), so a plain 401 may be either.

It is one rule with two readers, and they must not disagree. The transport reads it to decide whether a refused call ends a pooled credential at once or is confirmed first, and internal/toolutil reads it to decide what a 401 is described as to a model. A copy in either would let the server end a credential while telling the model a permission was missing, or the reverse.

It is not the rule the credential probe's answer is read by, [credentialVerdictFor], which is the opposite and is right where it is used: GET /user asks about the credential and nothing else, so any 401 or 403 there is about the credential.

func WithClient

func WithClient(ctx context.Context, c *Client) context.Context

WithClient returns ctx carrying the client every handler running under it must use.

It is the channel that lets one github.com/modelcontextprotocol/go-sdk/mcp.Server serve many credentials. A server built once per configuration shape registers handlers that capture the credential-less client from NewUnboundClient; the HTTP layer resolves the pool entry for each request and installs that entry's client here, and Client.For is what every handler reads it back through.

A nil client is not stored. "Bound to nothing" and "not bound" would then be indistinguishable to Client.For, and the difference decides whether a handler runs under the caller's credential or under the one that refuses every request.

func WriteCapable

func WriteCapable(scopes []string) bool

WriteCapable reports whether a token's scopes permit mutating GitLab.

Unknown scopes (nil: detection failed, was disabled, or the instance is too old to answer) count as write-capable. Assuming otherwise would silently strip every mutating tool from a deployment whose token is perfectly able to use them, and a wrong "no" is invisible — the tools are simply not there — while a wrong "yes" surfaces as GitLab's own 403 on the call that actually tried to write.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client wraps the official GitLab API client with project-specific configuration. It includes connection resilience: when GitLab is unreachable at startup, the server enters degraded mode and automatically recovers when connectivity is restored.

func ClientFrom

func ClientFrom(ctx context.Context) (*Client, bool)

ClientFrom returns the client bound to ctx, and whether there was one.

Client.For is what handlers use; this is for the few callers that must distinguish "no credential is bound" from "the fallback was returned", such as a middleware deciding whether it has anything to install.

func NewClient

func NewClient(cfg *config.Config) (*Client, error)

NewClient creates an authenticated GitLab client from the provided configuration. When cfg.SkipTLSVerify is true, TLS certificate verification is disabled (for self-signed certs). The client includes a resilience transport that enables automatic recovery when GitLab becomes available after being unreachable at startup.

func NewClientWithToken

func NewClientWithToken(baseURL, token string, skipTLSVerify bool) (*Client, error)

NewClientWithToken creates an authenticated GitLab client with explicit parameters. Unlike NewClient, it does not require a full config.Config and is designed for use in the server pool where each client has a unique token but shares the same base URL and TLS settings. The client includes a resilience transport that enables automatic recovery when GitLab becomes available after being unreachable.

func NewClientWithTokenRetries

func NewClientWithTokenRetries(baseURL, token string, skipTLSVerify, disableRetries bool) (*Client, error)

NewClientWithTokenRetries is NewClientWithToken with the retry policy exposed: disableRetries turns off client-go's retryablehttp wrapper (RetryMax 5, linear backoff), which unit tests need — a pooled client probing a mock that answers 5xx otherwise sleeps through the whole credential-check deadline instead of reading the answer it already has.

func NewOAuthClientWithToken

func NewOAuthClientWithToken(baseURL, token string, skipTLSVerify bool) (*Client, error)

NewOAuthClientWithToken creates a GitLab client that authenticates with "Authorization: Bearer" instead of the PRIVATE-TOKEN header. The server pool uses it in oauth HTTP mode, where every credential arrives as a Bearer token: an OAuth access token (gloas-...) is ONLY valid as Bearer — GitLab rejects it in PRIVATE-TOKEN — while personal access tokens are valid in both schemes, so forwarding exactly as received is correct for every token kind the mode admits.

func NewUnboundClient

func NewUnboundClient(baseURL string) *Client

NewUnboundClient returns a client for baseURL that carries no credential and refuses every request with ErrUnboundClient.

It exists for the catalog cached per configuration and shared by every pooled server. The catalog is built once, through the same ActionSpecs functions a bound client would be handed, and those inspect the client only for the instance class (GitLab.com or self-managed), which the URL answers. Building it with a real client would capture that client's credential in a process-wide cache; building it with this one captures nothing that can reach GitLab, so a handler served from the shared copy by mistake fails closed instead of running under someone else's token.

It panics when baseURL does not parse: the URL is a constant chosen by the caller, so a bad one is a programming error rather than a condition to handle.

func (*Client) CheckCredential added in v3.1.0

func (c *Client) CheckCredential(ctx context.Context) CredentialVerdict

CheckCredential asks GitLab whether it accepts this credential, and reports which of the three answers it got.

Three and not two, because two callers need different halves of them. Admission (Client.CredentialRejected) needs only to know whether GitLab refused, and admits on anything else so that an instance outage is not a total denial of service. The pool's confirmation of a 401 that named no cause needs to know whether GitLab accepted: keeping an entry and recording that its credential was just checked is only honest on a real answer, and a 500 read as "accepted" would push back the credential-age ceiling on the strength of a question GitLab never answered. The pool's periodic revalidation reads all three, through Client.CheckCredentialDetail so it can log what each was read from: it evicts on a refusal, stamps the entry on an acceptance, and counts anything else as no verdict, since treating a briefly unreachable GitLab as a refusal would evict every tenant at once.

It issues GET /api/v4/user through the raw health client rather than the SDK on purpose: client-go wraps requests in retryablehttp with RetryMax 5 and a linear backoff, which turns a refused connection or a struggling instance into seconds of stalling. A liveness question about a credential should be asked once and answered fast. The health client also does not report its 401s to the unauthorized hook, which is what lets the pool ask this question about a 401, or on its revalidation sweep, without the answer raising another.

The probe URL is built from the normalized base URL the operator configured, never from a request.

func (*Client) CheckCredentialDetail added in v3.1.0

func (c *Client) CheckCredentialDetail(ctx context.Context) CredentialCheck

CheckCredentialDetail is Client.CheckCredential with the cause kept.

The verdict alone serves admission and the pool's confirmation of a 401, which act on it and log nothing about the probe. The pool's periodic revalidation logs every round that reaches no verdict, and a warning that cannot say whether GitLab answered 500, 429 or a redirect, or never answered at all, gives an operator who sees it on every round nothing to go on.

func (*Client) CredentialRejected

func (c *Client) CredentialRejected(ctx context.Context) bool

CredentialRejected reports whether GitLab actively refuses this credential.

It is Client.CheckCredential read by admission: only an explicit 401 or 403 counts as a rejection, and every other outcome, a transport error, a 404 from a stubbed instance, a 5xx, means no verdict was obtained and is reported as false, so callers fail open.

func (*Client) CurrentUser

func (c *Client) CurrentUser(ctx context.Context) (*CurrentUserInfo, error)

CurrentUser returns the identity of the authenticated GitLab user. It calls the /user API endpoint and returns both the numeric ID and username.

func (*Client) CurrentUsername deprecated

func (c *Client) CurrentUsername(ctx context.Context) (string, error)

CurrentUsername returns the username of the authenticated GitLab user.

Deprecated: Use Client.CurrentUser which returns both ID and username.

func (*Client) DetectEnterprise

func (c *Client) DetectEnterprise(ctx context.Context, fallback bool) bool

DetectEnterprise updates the client edition flag from /api/v4/version when GitLab exposes it, returning fallback when the field is absent or detection fails.

func (*Client) DetectTier

func (c *Client) DetectTier(ctx context.Context) edition.Tier

DetectTier resolves the GitLab licensing tier and stores it on the client.

It asks two questions, because neither one answers for every deployment:

  • GET /license, which carries the instance's own plan and is **admin-only on self-managed**. It is the authoritative answer where it is available.
  • GET /namespaces, which carries a plan per namespace and is readable by any token, for the namespaces the caller administers. On GitLab.com that plan is the subscription; on self-managed it is "default" whatever the instance is licensed for, since a subscription is a GitLab.com concept.

Measured on 2026-09-22 against a licensed EE 19.3.1 and against GitLab.com: a non-admin token is refused /license with 403 on both, /namespaces reports "default" for admin and non-admin alike on self-managed, and reports the real plan on GitLab.com for each namespace the caller administers. So the second question rescues every GitLab.com caller, which is the population that used to fall back to Free unconditionally, and rescues nobody on self-managed.

Where neither answers, the tier is edition.Free as before. That is right on a CE instance and wrong on a licensed one the caller cannot read the license of, and the two are told apart by the edition rather than guessed at: an enterprise build that could not be resolved gets a warning naming the flag that settles it, and a CE build stays silent because Free is the truth there.

func (*Client) EnableLazyInit

func (c *Client) EnableLazyInit()

EnableLazyInit enables lazy re-initialization on subsequent API calls. Called when startup Initialize() fails so that the server can recover automatically when GitLab becomes available again.

func (*Client) EnsureInitialized

func (c *Client) EnsureInitialized(ctx context.Context)

EnsureInitialized attempts lazy re-initialization if the client was not initialized at startup (e.g. GitLab was down). This allows automatic recovery when GitLab becomes available again. Thread-safe via initMu. Includes a 30-second cooldown between attempts to avoid hammering GitLab. Called automatically by [resilienceTransport] on every SDK request.

func (*Client) For

func (c *Client) For(ctx context.Context) *Client

For returns the client bound to ctx, or the receiver when ctx carries none.

The receiver is the fallback rather than the answer, which is what makes the shared-server arrangement fail closed: on a shape server the captured client is the unbound one, so a handler reached without a bound context refuses with ErrUnboundClient instead of running under whichever credential happened to build the shape. On stdio, and in every test that registers with a real client, no context ever carries one and the receiver is simply returned, so the call is invisible.

A nil receiver is allowed and returns whatever the context carries, so a caller holding no fallback can still resolve one.

func (*Client) GL

func (c *Client) GL() *gl.Client

GL returns the underlying gitlab client for use in tool handlers.

func (*Client) Initialize

func (c *Client) Initialize(ctx context.Context) (string, error)

Initialize validates GitLab connectivity via a direct HTTP health check (bypassing the SDK transport chain to avoid recursion). On success it marks the client as initialized and returns the GitLab version string.

func (*Client) IsEnterprise

func (c *Client) IsEnterprise() bool

IsEnterprise reports whether the GitLab instance is Premium/Ultimate, derived from the resolved tier (tier >= Premium).

func (*Client) IsGitLabDotCom

func (c *Client) IsGitLabDotCom() bool

IsGitLabDotCom reports whether the client is configured for GitLab.com.

func (*Client) IsInitialized

func (c *Client) IsInitialized() bool

IsInitialized returns true if Initialize() completed successfully.

func (*Client) IsUnbound

func (c *Client) IsUnbound() bool

IsUnbound reports whether this client is the credential-less one, which refuses every request with ErrUnboundClient.

It exists so a surface can refuse early and say why. A resource read or a completion resolved to this client has not failed at GitLab and never will reach it: the request could not be attributed to a credential, which is a wiring defect on this side. Asked after Client.For, it is the question "did this request bring a credential", and answering it before the call is what keeps the caller from being told their token lacks access to something.

A nil client is not unbound: it is no client at all, which every reader already handles.

func (*Client) MarkInitialized

func (c *Client) MarkInitialized()

MarkInitialized sets the initialized flag without running the full Initialize flow. Intended for test setups where the client is preconfigured with a token or mock credentials.

func (*Client) MarkInstanceCallerNamed

func (c *Client) MarkInstanceCallerNamed()

MarkInstanceCallerNamed records that this client's instance was named by a caller rather than by the operator, which is the --allow-any-gitlab-url hatch, and puts its very first hop under tier B.

It is called by the server pool, which is the only place that knows: a client is built from a URL string, and the string looks identical whether --gitlab-url published it or a GITLAB-URL header did.

The default is the other way round because the alternative breaks every ordinary deployment: a client whose instance nobody marked is one the operator configured, and refusing those would refuse GitLab on localhost.

func (*Client) PinTier added in v3.1.0

func (c *Client) PinTier(t edition.Tier)

PinTier records a tier the operator named, and keeps it: nothing the client learns about the instance afterwards replaces it, a lazy re-initialization included. The catalog a process registers is built for this tier, so a handler reading Client.IsEnterprise must answer for the same one.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) (string, error)

Ping validates connectivity and authentication by calling the GitLab version endpoint. Returns the GitLab version string on success. Callers should wrap ctx with context.WithTimeout to bound the network round-trip.

func (*Client) SetEnterprise

func (c *Client) SetEnterprise(v bool)

SetEnterprise marks the client as connected to a Premium/Ultimate instance. It sets the tier to Premium when v is true and Free when v is false; callers needing the Premium/Ultimate distinction should use Client.SetTier. A tier set with Client.PinTier is left as it is, since the edition says less than the pin does.

func (*Client) SetMaxResponseBytes

func (c *Client) SetMaxResponseBytes(n int64)

SetMaxResponseBytes sets how much of a single response body this client will read. A value of zero or less removes the ceiling.

The ceiling applies from the next request onwards; a body already being read keeps the limit it started with.

func (*Client) SetOnUnauthorized

func (c *Client) SetOnUnauthorized(fn func(UnauthorizedAnswer))

SetOnUnauthorized registers fn to run when GitLab answers a call made with this client's credential with 401, and says what the 401 named. fn runs on the goroutine that made the call, so it must be cheap and must not block; a nil fn clears it.

The two answers arrive differently, because they mean different things. A 401 naming the credential (UnauthorizedCredential) is GitLab's verdict on it, and reaches fn once for the client's lifetime. A 401 naming nothing (UnauthorizedUnexplained) reaches fn every time: it may be a permission refusal of one call, which says nothing about the next, so it must neither stand for the credential's verdict nor use up the one delivery that verdict gets. Deciding how often to act on it is fn's business.

Only the SDK's requests are reported. The raw probes this client makes outside the SDK are not, since their callers act on the answer themselves.

func (*Client) SetTier

func (c *Client) SetTier(t edition.Tier)

SetTier records the resolved GitLab licensing tier for this client.

func (*Client) Tier

func (c *Client) Tier() edition.Tier

Tier returns the resolved GitLab licensing tier for this client.

func (*Client) TierPinned added in v3.1.0

func (c *Client) TierPinned() bool

TierPinned reports whether the tier was named by the operator rather than detected.

type CredentialCheck added in v3.1.0

type CredentialCheck struct {
	// Verdict is what [Client.CheckCredential] reports.
	Verdict CredentialVerdict
	// Status is the HTTP status GitLab answered the probe with, and 0 when no
	// response arrived.
	Status int
	// Err is nil when GitLab answered with a verdict. Otherwise it says why
	// there was none: the error that stopped the request from being built or
	// answered (a refused connection, a TLS failure, a timeout), or, when a
	// response arrived with a status that is neither verdict, that status.
	Err error
}

CredentialCheck is one credential probe's verdict together with what it was read from, for a caller that has to say why no verdict was reached.

type CredentialVerdict added in v3.1.0

type CredentialVerdict uint8

CredentialVerdict is what GitLab answered when asked whether it accepts a credential. See Client.CheckCredential.

const (
	// CredentialUnanswered means no verdict was obtained: a transport error, a
	// timeout, a 404 from a stubbed instance, a 5xx. It is the zero value,
	// because a question nobody answered must never read as either answer.
	CredentialUnanswered CredentialVerdict = iota
	// CredentialAccepted means GitLab answered the probe with a 2xx.
	CredentialAccepted
	// CredentialRefused means GitLab answered the probe with 401 or 403.
	CredentialRefused
)

type CurrentUserInfo

type CurrentUserInfo struct {
	UserID   int
	Username string
}

CurrentUserInfo holds the identity of the authenticated GitLab user.

type ResponseCapture

type ResponseCapture struct {
	// contains filtered or unexported fields
}

ResponseCapture holds the body of the last response a request made under WithResponseCapture received, so that a handler can read from it a field GitLab sends and client-go's struct does not model, from the same bytes the SDK decoded, without a second request and without spelling the route itself.

It exists because the 1:1 surface has no room for a field GitLab sends and this server drops, and client-go models most of the API but not all of it: a note's `imported_from`, a member's `membership_state`, a key's `expires_at`. Before this the answer was a request of the handler's own, which duplicates the route client-go already spells and, for a list, its pagination too. A capture leaves every request as it was and adds one decode of bytes already in memory.

Safe for use from one goroutine at a time, which is what a handler is; the transport records under a lock because a retried request answers more than once, and the last answer is the one the SDK decoded.

func CapturedBody

func CapturedBody(body []byte) *ResponseCapture

CapturedBody returns a capture already holding body, as if a request made under it had been answered with it. It is for a test of a reader that decodes captures, which would otherwise need a transport to hand it one.

func WithResponseCapture

func WithResponseCapture(ctx context.Context) (context.Context, *ResponseCapture)

WithResponseCapture returns a context under which the SDK client records the body of every response it receives, and the capture to read it from. Pass the context to the SDK call with gl.WithContext.

func (*ResponseCapture) Decode

func (c *ResponseCapture) Decode(v any) error

Decode unmarshals the captured body into v, the way the SDK unmarshalled it into its own struct. A body that does not decode is an error a handler reports rather than swallows: the SDK decoded the same bytes, so the fault is in the type v names.

type UnauthorizedAnswer added in v3.1.0

type UnauthorizedAnswer uint8

UnauthorizedAnswer is what a 401 GitLab answered says about the credential it refused.

GitLab answers 401 for two different things, and they must not be acted on alike. Its API guard answers it for a credential it cannot use, and at a family of REST routes GitLab's API helper unauthorized! (lib/api/helpers.rb) answers it for a valid credential that lacks a permission (entry 55 of docs/development/upstream-bugs.md). The first is a reason to stop serving the credential; the second is an ordinary refusal of one call, and ending the caller's subscriptions over it tells them to re-authenticate a token that works.

const (
	// UnauthorizedUnexplained is a 401 that names no cause. GitLab answers a
	// permission refusal and a token it no longer finds with the same bytes,
	// so the answer alone cannot say which it was, and whoever acts on it has
	// to ask again. It is the zero value on purpose: an answer nobody
	// classified is read the way that asks rather than the way that acts.
	UnauthorizedUnexplained UnauthorizedAnswer = iota
	// UnauthorizedCredential is a 401 GitLab said was about the credential
	// itself, which [UnauthorizedNamesCredential] decides.
	UnauthorizedCredential
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL