Commit 1d0699b6 authored by Patrick Rice's avatar Patrick Rice 🌆
Browse files

Merge branch 'config-custom-headers' into 'main'

feat(config): support custom headers for instances

See merge request !2431
parents b62b63a2 76b0e82a
Loading
Loading
Loading
Loading
Loading
+35 −1
Original line number Original line Diff line number Diff line
@@ -498,9 +498,22 @@ func (c *Config) NewClientForContext(name string, options ...gitlab.ClientOption
		return nil, err
		return nil, err
	}
	}


	defaultRequestOptions := []gitlab.RequestOptionFunc{
		gitlab.WithContext(c.ctx),
	}

	// custom headers
	customHeaders, err := c.resolveCustomHeaders(instance.CustomHeaders)
	if err != nil {
		return nil, err
	}
	if len(customHeaders) > 0 {
		defaultRequestOptions = append(defaultRequestOptions, gitlab.WithHeaders(customHeaders))
	}

	configOptions := []gitlab.ClientOptionFunc{
	configOptions := []gitlab.ClientOptionFunc{
		gitlab.WithBaseURL(baseURL.String()),
		gitlab.WithBaseURL(baseURL.String()),
		gitlab.WithRequestOptions(gitlab.WithContext(c.ctx)),
		gitlab.WithRequestOptions(defaultRequestOptions...),
	}
	}


	if c.config.Preferences.RetryMax != nil {
	if c.config.Preferences.RetryMax != nil {
@@ -842,6 +855,27 @@ func (c *Config) resolveOAuth2ClientSecret(o *v1beta1.OAuth2) (string, error) {
	}
	}
}
}


func (c *Config) resolveCustomHeaders(headers []*v1beta1.Header) (map[string]string, error) {
	hs := make(map[string]string, len(headers))

	for _, header := range headers {
		switch v := header.HeaderValue.(type) {
		case *v1beta1.Header_Value:
			hs[*header.Name] = v.Value
		case *v1beta1.Header_ValueFrom:
			csv, err := c.resolveCredentialSource(v.ValueFrom)
			if err != nil {
				return nil, err
			}
			hs[*header.Name] = csv
		default:
			panic(fmt.Sprintf("unexpected v1beta.HeaderValue type: %#v. Please report at https://gitlab.com/gitlab-org/api/client-go/-/issues", v))
		}
	}

	return hs, nil
}

func (c *Config) resolveCredentialSource(cs *v1beta1.CredentialSource) (string, error) {
func (c *Config) resolveCredentialSource(cs *v1beta1.CredentialSource) (string, error) {
	switch s := cs.Source.(type) {
	switch s := cs.Source.(type) {
	case *v1beta1.CredentialSource_EnvVar:
	case *v1beta1.CredentialSource_EnvVar:
+74 −0
Original line number Original line Diff line number Diff line
package config
package config


import (
import (
	"net/http"
	"testing"
	"testing"


	"github.com/MakeNowJust/heredoc/v2"
	"github.com/MakeNowJust/heredoc/v2"
@@ -1759,3 +1760,76 @@ func TestConfig_NewClientForContext_Error_MismatchedCertificateAndKey(t *testing
	assert.Nil(t, client)
	assert.Nil(t, client)
	assert.Contains(t, err.Error(), "tls: private key does not match public key")
	assert.Contains(t, err.Error(), "tls: private key does not match public key")
}
}

func TestConfig_NewClientForContext_Success_CustomHeaderLiteral(t *testing.T) {
	// GIVEN
	c, err := NewFromString(heredoc.Doc(`
		instances:
		  - name: example
		    server: https://gitlab.example.com
		    custom-headers:
		      - name: My-Custom-Header
		        value: my-custom-header-value

		auths:
		  - name: pat-value-user
		    auth_info:
		      personal-access-token:
		        token-source:
		          value: direct-token-value

		contexts:
		  - name: test-context
		    instance: example
		    auth: pat-value-user
	`))
	require.NoError(t, err)

	// WHEN
	client, err := c.NewClientForContext("test-context")
	require.NoError(t, err)

	req, err := client.NewRequest(http.MethodGet, "any-path", nil, nil)
	require.NoError(t, err)

	// THEN
	assert.Equal(t, "my-custom-header-value", req.Header.Get("My-Custom-Header"))
}

func TestConfig_NewClientForContext_Success_CustomHeader_FromSource(t *testing.T) {
	// GIVEN
	c, err := NewFromString(heredoc.Doc(`
		instances:
		  - name: example
		    server: https://gitlab.example.com
		    custom-headers:
		      - name: My-Custom-Header
		        value-from:
		          env-var: MY_CUSTOM_HEADER_VALUE

		auths:
		  - name: pat-value-user
		    auth_info:
		      personal-access-token:
		        token-source:
		          value: direct-token-value

		contexts:
		  - name: test-context
		    instance: example
		    auth: pat-value-user
	`))
	require.NoError(t, err)

	t.Setenv("MY_CUSTOM_HEADER_VALUE", "my-custom-header-value")

	// WHEN
	client, err := c.NewClientForContext("test-context")
	require.NoError(t, err)

	req, err := client.NewRequest(http.MethodGet, "any-path", nil, nil)
	require.NoError(t, err)

	// THEN
	assert.Equal(t, "my-custom-header-value", req.Header.Get("My-Custom-Header"))
}
+48 −0
Original line number Original line Diff line number Diff line
@@ -21,6 +21,9 @@ func main() {


	// Example 3: extensions
	// Example 3: extensions
	extensions()
	extensions()

	// Example 4: custom headers
	customHeaders()
}
}


func basicConfigExample() {
func basicConfigExample() {
@@ -147,3 +150,48 @@ func extensions() {


	fmt.Printf("Authenticated as: %s (%s)\n", user.Name, user.Username)
	fmt.Printf("Authenticated as: %s (%s)\n", user.Name, user.Username)
}
}

func customHeaders() {
	fmt.Println("=== Custom Headers ===")

	cfg, err := config.NewFromString(heredoc.Doc(`
		version: gitlab.com/config/v1beta1

		current-context: gitlab-com

		contexts:
		  - name: gitlab-com
		    instance: gitlab-com
		    auth: token-env

		instances:
		  - name: gitlab-com
		    server: https://gitlab.com
		    custom_headers:
		      - name: My-Custom-Header
		        value: my-header-value

		auths:
		  - name: token-env
		    auth-info:
		      personal-access-token:
		        token-source:
		          env_var: GITLAB_TOKEN
	`))
	if err != nil {
		log.Fatalf("Failed to create config: %v", err)
	}

	client, err := cfg.NewClient(gitlab.WithUserAgent("my-app"))
	if err != nil {
		log.Fatalf("Failed to create client: %v", err)
	}

	// Use the client
	user, _, err := client.Users.CurrentUser()
	if err != nil {
		log.Fatalf("Failed to get current user: %v", err)
	}

	fmt.Printf("Authenticated as: %s (%s)\n", user.Name, user.Username)
}
+211 −92

File changed.

Preview size limit exceeded, changes collapsed.

+26 −0
Original line number Original line Diff line number Diff line
@@ -137,6 +137,9 @@ message Instance {


  // extensions specifies arbitrary custom configuration
  // extensions specifies arbitrary custom configuration
  map<string, google.protobuf.Struct> extensions = 12 [json_name = "extensions"];
  map<string, google.protobuf.Struct> extensions = 12 [json_name = "extensions"];

  // custom_headers can contain a list of additional headers to add to every request.
  repeated Header custom_headers = 13 [json_name = "custom-headers"];
}
}


// RateLimit contains rate limiting configuration
// RateLimit contains rate limiting configuration
@@ -154,6 +157,29 @@ message RateLimit {
  ];
  ];
}
}


// Header contains a single HTTP header definition
message Header {
  // name contains the name of the header
  string name = 1 [
    json_name = "name",
    (buf.validate.field).required = true,
    (buf.validate.field).string.min_len = 3
  ];

  oneof header_value {
    option (buf.validate.oneof).required = true;

    // value contains the literal value of the header
    string value = 2 [
      json_name = "value",
      (buf.validate.field).string.min_len = 1
    ];

    // value_from contains a source from where to retrieve the value from.
    CredentialSource value_from = 3 [json_name = "value-from"];
  }
}

// Context represents a combination of instance and auth
// Context represents a combination of instance and auth
message Context {
message Context {
  // name is the unique identifier for this context
  // name is the unique identifier for this context