diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..572127867 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(yamllint:*)", + "Bash(make lint:*)" + ], + "deny": [], + "ask": [], + "defaultMode": "acceptEdits" + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..85c0840ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,128 @@ +--- +name: CI + +on: + push: + branches: [main, rhobs-obs-api-konflux] + pull_request: + branches: [main, rhobs-obs-api-konflux] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Build + run: | + make build + git diff --exit-code + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y xz-utils unzip shellcheck + + - name: Install bingo + run: go install github.com/bwplotka/bingo@latest + + - name: Lint + run: make lint --always-make + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Install dependencies + run: | + sudo apt-get update && sudo apt-get -y install xz-utils unzip openssl + + - name: Test + run: make test --always-make + + test-e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y make + + - name: Build e2e test container + run: | + echo "Building e2e test container..." + OCI_BIN=docker make container-test + echo "Available Docker images:" + docker images | grep observatorium || echo "No observatorium images found" + echo "Saving container image to cache..." + docker save quay.io/observatorium/api:local_e2e_test > /tmp/e2e-image.tar + + - name: Load e2e test container + run: | + echo "Loading e2e test container..." + docker load < /tmp/e2e-image.tar + echo "Verifying image is loaded:" + docker images | grep observatorium + + - name: End-to-end tests + run: | + echo "Running e2e tests..." + rm -rf test/e2e/e2e_* + OCI_BIN=docker CGO_ENABLED=1 GO111MODULE=on go test -timeout=25m -race -short -tags integration ./test/e2e 2>&1 | tee test_output.log + exit_code=${PIPESTATUS[0]} + if [ $exit_code -ne 0 ]; then + echo "Tests failed with exit code: $exit_code" + echo "Last 50 lines of output:" + tail -50 test_output.log + fi + exit $exit_code + + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get -y install unzip + + - name: Install bingo + run: go install github.com/bwplotka/bingo@latest + + - name: Generate and validate + run: | + make generate validate --always-make + make proto + git diff --exit-code \ No newline at end of file diff --git a/README.md b/README.md index 39542a7b4..9a6f50c2b 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ Usage of ./observatorium-api: The endpoint against which to send read requests for metrics. -metrics.rules.endpoint string The endpoint against which to make get requests for listing recording/alerting rules and put requests for creating/updating recording/alerting rules. + -metrics.status.endpoint string + The endpoint against which to make requests for status information about metrics (e.g. '/api/v1/status/tsdb'). -metrics.tenant-header string The name of the HTTP header containing the tenant ID to forward to the metrics upstreams. (default "THANOS-TENANT") -metrics.tenant-label string diff --git a/authentication/authentication_test.go b/authentication/authentication_test.go index 43e8d48e7..b872de26f 100644 --- a/authentication/authentication_test.go +++ b/authentication/authentication_test.go @@ -120,46 +120,46 @@ func TestTokenExpiredErrorHandling(t *testing.T) { expiredErr := &oidc.TokenExpiredError{ Expiry: time.Now().Add(-time.Hour), // Expired an hour ago } - + // Test direct error var tokenExpiredErr *oidc.TokenExpiredError if !errors.As(expiredErr, &tokenExpiredErr) { t.Error("errors.As should identify TokenExpiredError") } - + // Test wrapped error wrappedErr := &wrappedError{ msg: "verification failed", err: expiredErr, } - + if !errors.As(wrappedErr, &tokenExpiredErr) { t.Error("errors.As should identify wrapped TokenExpiredError") } }) - + t.Run("Other errors are not identified as TokenExpiredError", func(t *testing.T) { // Test with a generic error genericErr := errors.New("generic verification error") - + var tokenExpiredErr *oidc.TokenExpiredError if errors.As(genericErr, &tokenExpiredErr) { t.Error("errors.As should not identify generic error as TokenExpiredError") } - + // Test with wrapped generic error wrappedGenericErr := &wrappedError{ msg: "verification failed", err: genericErr, } - + if errors.As(wrappedGenericErr, &tokenExpiredErr) { t.Error("errors.As should not identify wrapped generic error as TokenExpiredError") } }) } -// Helper type to wrap errors for testing +// Helper type to wrap errors for testing. type wrappedError struct { msg string err error diff --git a/authentication/mtls.go b/authentication/mtls.go index a09dc7c81..972141faa 100644 --- a/authentication/mtls.go +++ b/authentication/mtls.go @@ -192,4 +192,3 @@ func (a MTLSAuthenticator) GRPCMiddleware() grpc.StreamServerInterceptor { func (a MTLSAuthenticator) Handler() (string, http.Handler) { return "", nil } - diff --git a/authentication/mtls_test.go b/authentication/mtls_test.go index 591877eec..d3100c6fd 100644 --- a/authentication/mtls_test.go +++ b/authentication/mtls_test.go @@ -10,10 +10,11 @@ import ( "testing" "github.com/go-kit/log" + "github.com/observatorium/api/test/testtls" ) -// Helper function to generate test certificates using the existing testtls package +// Helper function to generate test certificates using the existing testtls package. func setupTestCertificatesWithFile(t testing.TB) (clientCert tls.Certificate, caPath string, cleanup func()) { t.Helper() @@ -26,10 +27,10 @@ func setupTestCertificatesWithFile(t testing.TB) (clientCert tls.Certificate, ca // Generate certificates using the testtls package err = testtls.GenerateCerts( tmpDir, - "test-api", // API common name + "test-api", // API common name []string{"localhost", "127.0.0.1"}, // API SANs - "test-dex", // Dex common name - []string{"localhost"}, // Dex SANs + "test-dex", // Dex common name + []string{"localhost"}, // Dex SANs ) if err != nil { os.RemoveAll(tmpDir) @@ -70,12 +71,12 @@ func TestMTLSAuthenticator_PathBasedAuthentication(t *testing.T) { defer cleanup() tests := []struct { - name string - pathPatterns []string - requestPath string - expectMTLS bool - expectError bool - description string + name string + pathPatterns []string + requestPath string + expectMTLS bool + expectError bool + description string }{ { name: "no_patterns_enforces_all_paths", @@ -139,7 +140,7 @@ func TestMTLSAuthenticator_PathBasedAuthentication(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create mTLS config with path patterns using file-based CA config := map[string]interface{}{ - "caPath": caPath, // Use file-based CA as original code expects + "caPath": caPath, // Use file-based CA as original code expects "pathPatterns": tt.pathPatterns, } @@ -319,7 +320,7 @@ func TestMTLSAuthenticator_InvalidPathPattern(t *testing.T) { } } -// Test path matching logic without requiring certificate validation +// Test path matching logic without requiring certificate validation. func TestMTLSAuthenticator_PathMatchingLogic(t *testing.T) { tests := []struct { name string @@ -338,7 +339,7 @@ func TestMTLSAuthenticator_PathMatchingLogic(t *testing.T) { { name: "pattern_matches_requires_mtls", pathPatterns: []string{"/api/.*/receive"}, - requestPath: "/api/metrics/v1/receive", + requestPath: "/api/metrics/v1/receive", expectSkip: false, description: "Matching pattern requires mTLS", }, @@ -379,7 +380,7 @@ func TestMTLSAuthenticator_PathMatchingLogic(t *testing.T) { } middleware := authenticator.Middleware() - + handlerCalled := false testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handlerCalled = true @@ -413,7 +414,7 @@ func TestMTLSAuthenticator_PathMatchingLogic(t *testing.T) { } } -// Test both CA configuration methods work correctly +// Test both CA configuration methods work correctly. func TestMTLSAuthenticator_CAConfiguration(t *testing.T) { // Test file-based CA configuration t.Run("file_based_ca", func(t *testing.T) { @@ -449,7 +450,7 @@ func TestMTLSAuthenticator_CAConfiguration(t *testing.T) { } config := map[string]interface{}{ - "ca": caPEM, // Direct CA data + "ca": caPEM, // Direct CA data } logger := log.NewNopLogger() @@ -465,4 +466,3 @@ func TestMTLSAuthenticator_CAConfiguration(t *testing.T) { } }) } - diff --git a/authentication/oidc.go b/authentication/oidc.go index 288fcd2db..d38637384 100644 --- a/authentication/oidc.go +++ b/authentication/oidc.go @@ -42,11 +42,11 @@ func init() { // oidcConfig represents the oidc authenticator config. type oidcConfig struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - GroupClaim string `json:"groupClaim"` - IssuerRawCA []byte `json:"issuerCA"` - IssuerCAPath string `json:"issuerCAPath"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + GroupClaim string `json:"groupClaim"` + IssuerRawCA []byte `json:"issuerCA"` + IssuerCAPath string `json:"issuerCAPath"` issuerCA *x509.Certificate IssuerURL string `json:"issuerURL"` RedirectURL string `json:"redirectURL"` @@ -299,7 +299,7 @@ func (a oidcAuthenticator) Middleware() Middleware { break } } - + // If path doesn't match, skip OIDC enforcement if !pathMatches { next.ServeHTTP(w, r) diff --git a/authentication/oidc_test.go b/authentication/oidc_test.go index 8071bae83..fc70025a7 100644 --- a/authentication/oidc_test.go +++ b/authentication/oidc_test.go @@ -88,7 +88,7 @@ func TestOIDCPathMatching(t *testing.T) { } if shouldSkip != tt.expectSkip { - t.Errorf("Expected skip=%v, got skip=%v for path %q with patterns %v", + t.Errorf("Expected skip=%v, got skip=%v for path %q with patterns %v", tt.expectSkip, shouldSkip, tt.requestPath, tt.pathPatterns) } }) @@ -98,11 +98,11 @@ func TestOIDCPathMatching(t *testing.T) { func TestOIDCConfigPathPatternsIntegration(t *testing.T) { // Test that path patterns are correctly passed to the OIDC authenticator config tests := []struct { - name string - configData map[string]interface{} - expectError bool - expectPaths []string - description string + name string + configData map[string]interface{} + expectError bool + expectPaths []string + description string }{ { name: "valid_path_patterns", @@ -116,7 +116,7 @@ func TestOIDCConfigPathPatternsIntegration(t *testing.T) { description: "Valid path patterns should be accepted", }, { - name: "empty_path_patterns", + name: "empty_path_patterns", configData: map[string]interface{}{ "pathPatterns": []string{}, "clientID": "test-client", @@ -184,6 +184,9 @@ func TestOIDCConfigPathPatternsIntegration(t *testing.T) { if len(config.PathPatterns) != len(tt.expectPaths) { t.Errorf("Expected %d path patterns, got %d", len(tt.expectPaths), len(config.PathPatterns)) } + if len(pathMatchers) != len(tt.expectPaths) { + t.Errorf("Expected %d compiled matchers, got %d", len(tt.expectPaths), len(pathMatchers)) + } for i, expected := range tt.expectPaths { if i >= len(config.PathPatterns) { @@ -214,7 +217,7 @@ func TestOIDCMiddlewareActual(t *testing.T) { expectSkipped: true, }, { - name: "matching_path_not_skipped", + name: "matching_path_not_skipped", pathPatterns: []string{"/api/.*/query"}, requestPath: "/api/metrics/v1/query", expectSkipped: false, @@ -266,4 +269,3 @@ func TestOIDCMiddlewareActual(t *testing.T) { }) } } - diff --git a/main.go b/main.go index a7688e23b..3f0bf65bc 100644 --- a/main.go +++ b/main.go @@ -1605,7 +1605,6 @@ func tenantAuthenticatorConfig(t *tenant) (map[string]interface{}, string, error } } - type otelErrorHandler struct { logger log.Logger } diff --git a/main_test.go b/main_test.go index a013559b7..faf3d323a 100644 --- a/main_test.go +++ b/main_test.go @@ -195,13 +195,13 @@ tenants: func TestPathMatchingBehavior(t *testing.T) { tests := []struct { - name string - oidcPaths []string - mtlsPaths []string - testPath string - expectOIDC bool - expectMTLS bool - description string + name string + oidcPaths []string + mtlsPaths []string + testPath string + expectOIDC bool + expectMTLS bool + description string }{ { name: "read_path_oidc_only", @@ -250,9 +250,9 @@ func TestPathMatchingBehavior(t *testing.T) { }, { name: "case_sensitive_matching", - oidcPaths: []string{"/api/.*/Query"}, // uppercase Q + oidcPaths: []string{"/api/.*/Query"}, // uppercase Q mtlsPaths: []string{"/api/.*/receive"}, - testPath: "/api/metrics/v1/query", // lowercase q + testPath: "/api/metrics/v1/query", // lowercase q expectOIDC: false, expectMTLS: false, description: "Pattern matching should be case sensitive", @@ -310,5 +310,3 @@ func TestPathMatchingBehavior(t *testing.T) { }) } } - -