Skip to content

Conversation

@vortegatorres
Copy link
Contributor

@vortegatorres vortegatorres commented Nov 14, 2025

What?

Implements #5412. Adds a new url secret-source that fetches secrets from HTTP endpoints. This allows k6 to retrieve secrets from any HTTP-based secret management service using configurable URL templates.

Why?

  • k6 Cloud: Enables the secrets feature for k6 Cloud without requiring custom extensions
  • OSS Users: Provides native integration with HTTP-based secret management systems

Checklist

  • I have performed a self-review of my code.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests for my changes.
  • I have run linter and tests locally (make check) and all pass.

Checklist: Documentation (only for k6 maintainers and if relevant)

Please do not merge this PR until the following items are filled out.

  • I have added the correct milestone and labels to the PR.
  • I have updated the release notes: link
  • I have updated or added an issue to the k6-documentation: grafana/k6-docs#NUMBER if applicable
  • I have updated or added an issue to the TypeScript definitions: grafana/k6-DefinitelyTyped#NUMBER if applicable

Related PR(s)/Issue(s)

https://github.com/grafana/k6-cloud/issues/4038

Closes #5412

@vortegatorres vortegatorres self-assigned this Nov 14, 2025
@vortegatorres vortegatorres requested a review from a team as a code owner November 14, 2025 15:21
@vortegatorres vortegatorres requested review from ankur22 and oleiade and removed request for a team November 14, 2025 15:21
@vortegatorres
Copy link
Contributor Author

Test:

  • Given this mock server:
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
)

// Response structure matching GSM API format
type secretResponse struct {
	Plaintext string `json:"plaintext"`
	Name      string `json:"name"`
}

// Mock secret store - in real GSM, these would be encrypted secrets
var mockSecrets = map[string]string{
	"api-key":        "super-secret-api-key-12345",
	"database-pass":  "db-password-xyz789",
	"jwt-secret":     "jwt-signing-key-abc123",
	"stripe-key":     "sk_test_mock_stripe_key",
	"github-token":   "ghp_mock_github_token_12345",
	"test-secret":    "this-is-a-test-secret",
	"my-secret":      "my-secret-value",
	"another-secret": "another-secret-value",
}

func main() {
	mux := http.NewServeMux()

	// Handle secret decryption requests
	mux.HandleFunc("/secrets/", handleSecretRequest)

	// Health check endpoint
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"status":"healthy"}`))
	})

	addr := ":8888"
	log.Printf("Starting mock GSM server on %s", addr)
	log.Printf("Available secrets: %v", getSecretKeys())
	log.Printf("Expected Authorization header: Bearer YOUR_GSM_TOKEN_HERE")
	log.Println()
	log.Println("Example curl command:")
	log.Println(`  curl -H "Authorization: Bearer YOUR_GSM_TOKEN_HERE" http://localhost:8888/secrets/api-key/decrypt`)

	if err := http.ListenAndServe(addr, mux); err != nil {
		log.Fatalf("Server failed to start: %v", err)
	}
}

func handleSecretRequest(w http.ResponseWriter, r *http.Request) {
	// Log incoming request
	log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)

	// Check HTTP method
	if r.Method != http.MethodGet {
		log.Printf("  ❌ Invalid method: %s", r.Method)
		http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	// Check Authorization header
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" {
		log.Println("  ❌ Missing Authorization header")
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(`{"error":"missing authorization header"}`))
		return
	}

	// Validate Bearer token
	expectedToken := "Bearer YOUR_GSM_TOKEN_HERE"
	if authHeader != expectedToken {
		log.Printf("  ❌ Invalid token: %s", authHeader)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(`{"error":"invalid authorization token"}`))
		return
	}

	// Extract secret key from URL path
	// Expected format: /secrets/{key}/decrypt
	path := strings.TrimPrefix(r.URL.Path, "/secrets/")
	path = strings.TrimSuffix(path, "/decrypt")

	if path == "" || path == r.URL.Path {
		log.Println("  ❌ Invalid path format")
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write([]byte(`{"error":"invalid path format, expected /secrets/{key}/decrypt"}`))
		return
	}

	secretKey := path

	// Look up secret
	secretValue, exists := mockSecrets[secretKey]
	if !exists {
		log.Printf("  ❌ Secret not found: %s", secretKey)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusNotFound)
		_, _ = w.Write([]byte(fmt.Sprintf(`{"error":"secret %q not found"}`, secretKey)))
		return
	}

	// Build response
	response := secretResponse{
		Plaintext: secretValue,
		Name:      fmt.Sprintf("projects/test-project/secrets/%s", secretKey),
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	if err := json.NewEncoder(w).Encode(response); err != nil {
		log.Printf("  ❌ Failed to encode response: %v", err)
		return
	}

	log.Printf("  ✅ Returned secret: %s (length: %d)", secretKey, len(secretValue))
}

func getSecretKeys() []string {
	keys := make([]string, 0, len(mockSecrets))
	for k := range mockSecrets {
		keys = append(keys, k)
	}
	return keys
}
  • And this configuration:
{
  "urlTemplate": "http://localhost:8888/secrets/{key}/decrypt",
  "method": "GET",
  "headers": {
    "Authorization": "Bearer YOUR_GSM_TOKEN_HERE"
  },
  "responsePath": "plaintext",
  "requestsPerMinuteLimit": 300,
  "requestsBurst": 10,
  "timeoutSeconds": 30
}
  • And this k6 script:
import secrets from 'k6/secrets';

export default async () => {
  console.log('Fetching secret from URL source...');
  const my_secret = await secrets.get('api-key');
  console.log(my_secret == "super-secret-api-key-12345");
};
  • It worked 🚀
./k6 run --secret-source=url=config=examples/secrets/url-gsm-local.json examples/secrets/url-test.js

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: examples/secrets/url-test.js
        output: -

     scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
              * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)

INFO[0000] Fetching secret from URL source...            source=console
INFO[0000] true                                          source=console


  █ TOTAL RESULTS 

    EXECUTION
    iteration_duration...: avg=7.22ms min=7.22ms med=7.22ms max=7.22ms p(90)=7.22ms p(95)=7.22ms
    iterations...........: 1   127.469726/s

    NETWORK
    data_received........: 0 B 0 B/s
    data_sent............: 0 B 0 B/s




running (00m00.0s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m00.0s/10m0s  1/1 iters, 1 per VU

@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 14, 2025 15:27 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 14, 2025 15:29 — with GitHub Actions Inactive
@mstoykov mstoykov added this to the v1.5.0 milestone Nov 17, 2025
@oleiade oleiade requested a review from mstoykov November 17, 2025 09:23
@oleiade
Copy link
Contributor

oleiade commented Nov 17, 2025

I'm reviewing this, but added you @mstoykov as a reviewer too, as I really want your eyes on this 🙇🏻

ankur22
ankur22 previously approved these changes Nov 17, 2025
Copy link
Contributor

@ankur22 ankur22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀 I don't have the full context of the changes, but from I can see and the issue description it makes sense with this approach.

}
defer func() { _ = response.Body.Close() }()

if response.StatusCode != http.StatusOK {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a plan to retry server errors? At the moment the iteration will fail when it sees a 5xx, subsequent iterations might pass though if the error is temporary. I guess with retries, the issue is that it will affect the timings of the iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I added retries in d625ce9

@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:18 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:19 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:30 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:31 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:38 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 17, 2025 13:40 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 09:46 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:00 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:02 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:08 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:11 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:41 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 10:43 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 11:23 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 11:25 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 11:39 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 11:41 — with GitHub Actions Inactive
@vortegatorres
Copy link
Contributor Author

@vortegatorres Can we maybe have the mock server as a submodule, as we do for the grpc examples. I do remember there were some lint problems, we can fix.

Also maybe you can rebase and squash the commits at this point as I see yopu had some renovate.json changes that shouldn't be here

@mstoykov I added a mock server in 38e783e

@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 12:41 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 18, 2025 12:43 — with GitHub Actions Inactive
@mstoykov mstoykov requested review from ankur22 and oleiade November 19, 2025 17:35
Copy link
Contributor

@ankur22 ankur22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGMT 🚀

@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 20, 2025 10:56 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 20, 2025 10:58 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 20, 2025 21:57 — with GitHub Actions Inactive
@vortegatorres vortegatorres temporarily deployed to azure-trusted-signing November 20, 2025 21:59 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add URL-based secret-source to support k6 cloud secrets and OSS use cases

4 participants