From 3012f9a1b282a08a6fe667198cbe674272bf45d6 Mon Sep 17 00:00:00 2001 From: JaredforReal Date: Tue, 16 Dec 2025 00:00:55 +0800 Subject: [PATCH 1/2] add tests for pii and tls module Signed-off-by: JaredforReal --- .../pkg/utils/pii/policy_test.go | 552 ++++++++++++++++++ src/semantic-router/pkg/utils/tls/tls_test.go | 265 +++++++++ 2 files changed, 817 insertions(+) create mode 100644 src/semantic-router/pkg/utils/pii/policy_test.go create mode 100644 src/semantic-router/pkg/utils/tls/tls_test.go diff --git a/src/semantic-router/pkg/utils/pii/policy_test.go b/src/semantic-router/pkg/utils/pii/policy_test.go new file mode 100644 index 0000000000..8d650055b6 --- /dev/null +++ b/src/semantic-router/pkg/utils/pii/policy_test.go @@ -0,0 +1,552 @@ +package pii + +import ( + "testing" + + "github.com/vllm-project/semantic-router/src/semantic-router/pkg/config" +) + +func TestIsPIIEnabled(t *testing.T) { + tests := []struct { + name string + decisionName string + setupConfig func() *config.RouterConfig + expected bool + }{ + { + name: "Empty decision name", + decisionName: "", + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{} + }, + expected: false, + }, + { + name: "Decision not found", + decisionName: "nonexistent", + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{}, + }, + } + }, + expected: false, + }, + { + name: "PII enabled for decision", + decisionName: "finance", + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "finance", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + }, + }, + }, + }, + }, + }, + } + }, + expected: true, + }, + { + name: "PII disabled for decision", + decisionName: "general", + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "general", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": false, + }, + }, + }, + }, + }, + }, + } + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := tt.setupConfig() + checker := NewPolicyChecker(cfg) + result := checker.IsPIIEnabled(tt.decisionName) + if result != tt.expected { + t.Errorf("IsPIIEnabled() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestCheckPolicy(t *testing.T) { + tests := []struct { + name string + decisionName string + detectedPII []string + setupConfig func() *config.RouterConfig + expectAllowed bool + expectDenied []string + }{ + { + name: "PII disabled - allow all", + decisionName: "general", + detectedPII: []string{"PERSON", "EMAIL_ADDRESS"}, + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "general", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": false, + }, + }, + }, + }, + }, + }, + } + }, + expectAllowed: true, + expectDenied: nil, + }, + { + name: "PII enabled with allowed types", + decisionName: "public", + detectedPII: []string{"PERSON", "ORGANIZATION"}, + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "public", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + "pii_types_allowed": []interface{}{ + "PERSON", + "ORGANIZATION", + }, + }, + }, + }, + }, + }, + }, + } + }, + expectAllowed: true, + expectDenied: nil, + }, + { + name: "Deny by default with allowed types", + decisionName: "restricted", + detectedPII: []string{"PERSON", "EMAIL_ADDRESS", "CREDIT_CARD"}, + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "restricted", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + "allow_by_default": false, + "pii_types_allowed": []interface{}{ + "PERSON", + "EMAIL_ADDRESS", + }, + }, + }, + }, + }, + }, + }, + } + }, + expectAllowed: false, + expectDenied: []string{"CREDIT_CARD"}, + }, + { + name: "NO_PII should be skipped", + decisionName: "restricted", + detectedPII: []string{"NO_PII", "PERSON"}, + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "restricted", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + "allow_by_default": false, + "pii_types_allowed": []interface{}{"PERSON"}, + }, + }, + }, + }, + }, + }, + } + }, + expectAllowed: true, + expectDenied: nil, + }, + { + name: "All PII types allowed", + decisionName: "restricted", + detectedPII: []string{"PERSON", "EMAIL_ADDRESS"}, + setupConfig: func() *config.RouterConfig { + return &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "restricted", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + "allow_by_default": false, + "pii_types_allowed": []interface{}{ + "PERSON", + "EMAIL_ADDRESS", + }, + }, + }, + }, + }, + }, + }, + } + }, + expectAllowed: true, + expectDenied: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := tt.setupConfig() + checker := NewPolicyChecker(cfg) + allowed, denied, err := checker.CheckPolicy(tt.decisionName, tt.detectedPII) + if err != nil { + t.Errorf("CheckPolicy() returned unexpected error: %v", err) + } + + if allowed != tt.expectAllowed { + t.Errorf("CheckPolicy() allowed = %v, want %v", allowed, tt.expectAllowed) + } + + if len(denied) != len(tt.expectDenied) { + t.Errorf("CheckPolicy() denied = %v, want %v", denied, tt.expectDenied) + } + + // Check if denied types match + if tt.expectDenied != nil { + for _, expectedDenied := range tt.expectDenied { + found := false + for _, d := range denied { + if d == expectedDenied { + found = true + break + } + } + if !found { + t.Errorf("Expected denied PII type %s not found in result", expectedDenied) + } + } + } + }) + } +} + +func TestIsPIITypeAllowed(t *testing.T) { + tests := []struct { + name string + piiType string + allowedTypes []string + expected bool + }{ + { + name: "Exact match", + piiType: "PERSON", + allowedTypes: []string{"PERSON", "ORGANIZATION"}, + expected: true, + }, + { + name: "Not in allowed list", + piiType: "CREDIT_CARD", + allowedTypes: []string{"PERSON", "ORGANIZATION"}, + expected: false, + }, + { + name: "BIO tag - B prefix", + piiType: "B-ORGANIZATION", + allowedTypes: []string{"ORGANIZATION"}, + expected: true, + }, + { + name: "BIO tag - I prefix", + piiType: "I-PERSON", + allowedTypes: []string{"PERSON"}, + expected: true, + }, + { + name: "BIO tag - O prefix", + piiType: "O-EMAIL_ADDRESS", + allowedTypes: []string{"EMAIL_ADDRESS"}, + expected: true, + }, + { + name: "BIO tag - E prefix", + piiType: "E-PHONE_NUMBER", + allowedTypes: []string{"PHONE_NUMBER"}, + expected: true, + }, + { + name: "BIO tag not allowed", + piiType: "B-CREDIT_CARD", + allowedTypes: []string{"PERSON"}, + expected: false, + }, + { + name: "Invalid BIO format", + piiType: "X-PERSON", + allowedTypes: []string{"PERSON"}, + expected: false, + }, + { + name: "Empty allowed list", + piiType: "PERSON", + allowedTypes: []string{}, + expected: false, + }, + { + name: "BIO tag with exact match also in list", + piiType: "B-PERSON", + allowedTypes: []string{"B-PERSON", "PERSON"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isPIITypeAllowed(tt.piiType, tt.allowedTypes) + if result != tt.expected { + t.Errorf("isPIITypeAllowed(%s, %v) = %v, want %v", + tt.piiType, tt.allowedTypes, result, tt.expected) + } + }) + } +} + +func TestExtractAllContent(t *testing.T) { + tests := []struct { + name string + userContent string + nonUserMessages []string + expectedLength int + expectedFirstItem string + }{ + { + name: "Both user and non-user content", + userContent: "What is the capital of France?", + nonUserMessages: []string{"Paris is the capital", "Population is 2.1M"}, + expectedLength: 3, + expectedFirstItem: "What is the capital of France?", + }, + { + name: "Only user content", + userContent: "Hello world", + nonUserMessages: []string{}, + expectedLength: 1, + expectedFirstItem: "Hello world", + }, + { + name: "Only non-user messages", + userContent: "", + nonUserMessages: []string{"System message 1", "System message 2"}, + expectedLength: 2, + expectedFirstItem: "System message 1", + }, + { + name: "Empty content", + userContent: "", + nonUserMessages: []string{}, + expectedLength: 0, + expectedFirstItem: "", + }, + { + name: "Multiple non-user messages", + userContent: "User query", + nonUserMessages: []string{"Msg1", "Msg2", "Msg3", "Msg4"}, + expectedLength: 5, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractAllContent(tt.userContent, tt.nonUserMessages) + + if len(result) != tt.expectedLength { + t.Errorf("ExtractAllContent() length = %d, want %d", len(result), tt.expectedLength) + } + + if tt.expectedLength > 0 && tt.expectedFirstItem != "" { + if result[0] != tt.expectedFirstItem { + t.Errorf("ExtractAllContent() first item = %s, want %s", + result[0], tt.expectedFirstItem) + } + } + }) + } +} + +func TestNewPolicyChecker(t *testing.T) { + cfg := &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "test-decision", + }, + }, + }, + } + + checker := NewPolicyChecker(cfg) + + if checker == nil { + t.Fatal("NewPolicyChecker() returned nil") + } + + if checker.Config == nil { + t.Error("PolicyChecker.Config is nil") + } + + if len(checker.Config.Decisions) != 1 { + t.Errorf("Expected 1 decision, got %d", len(checker.Config.Decisions)) + } +} + +func TestCheckPolicy_NilDecision(t *testing.T) { + cfg := &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{}, + }, + } + + checker := NewPolicyChecker(cfg) + allowed, denied, err := checker.CheckPolicy("nonexistent", []string{"PERSON"}) + if err != nil { + t.Errorf("CheckPolicy() returned unexpected error: %v", err) + } + + if !allowed { + t.Error("CheckPolicy() should allow when decision not found") + } + + if len(denied) > 0 { + t.Errorf("CheckPolicy() should return nil denied list when decision not found, got %v", denied) + } +} + +func TestCheckPolicy_ComplexScenario(t *testing.T) { + cfg := &config.RouterConfig{ + IntelligentRouting: config.IntelligentRouting{ + Decisions: []config.Decision{ + { + Name: "banking", + Plugins: []config.DecisionPlugin{ + { + Type: "pii", + Configuration: map[string]interface{}{ + "enabled": true, + "allow_by_default": false, + "pii_types_allowed": []interface{}{ + "PERSON", + "DATE_TIME", + "ORGANIZATION", + }, + }, + }, + }, + }, + }, + }, + } + + checker := NewPolicyChecker(cfg) + + testCases := []struct { + detected []string + expectAllowed bool + expectDenied []string + }{ + { + detected: []string{"PERSON", "DATE_TIME"}, + expectAllowed: true, + expectDenied: nil, + }, + { + detected: []string{"PERSON", "CREDIT_CARD", "US_SSN"}, + expectAllowed: false, + expectDenied: []string{"CREDIT_CARD", "US_SSN"}, + }, + { + detected: []string{"NO_PII"}, + expectAllowed: true, + expectDenied: nil, + }, + { + detected: []string{"B-PERSON", "I-ORGANIZATION"}, + expectAllowed: true, + expectDenied: nil, + }, + { + detected: []string{"B-PERSON", "CREDIT_CARD", "I-ORGANIZATION"}, + expectAllowed: false, + expectDenied: []string{"CREDIT_CARD"}, + }, + } + + for i, tc := range testCases { + allowed, denied, err := checker.CheckPolicy("banking", tc.detected) + if err != nil { + t.Errorf("Test case %d: unexpected error: %v", i, err) + } + if allowed != tc.expectAllowed { + t.Errorf("Test case %d: allowed = %v, want %v", i, allowed, tc.expectAllowed) + } + if len(denied) != len(tc.expectDenied) { + t.Errorf("Test case %d: denied length = %d, want %d", i, len(denied), len(tc.expectDenied)) + } + } +} diff --git a/src/semantic-router/pkg/utils/tls/tls_test.go b/src/semantic-router/pkg/utils/tls/tls_test.go new file mode 100644 index 0000000000..5e3205983b --- /dev/null +++ b/src/semantic-router/pkg/utils/tls/tls_test.go @@ -0,0 +1,265 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "testing" + "time" +) + +func TestCreateSelfSignedTLSCertificate(t *testing.T) { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + // Verify certificate is not empty + if len(cert.Certificate) == 0 { + t.Fatal("Certificate chain is empty") + } + + // Parse the certificate to verify structure + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse certificate: %v", err) + } + + // Verify certificate properties + t.Run("Organization", func(t *testing.T) { + if len(x509Cert.Subject.Organization) == 0 { + t.Error("Certificate has no organization") + } + if x509Cert.Subject.Organization[0] != "Inference Ext" { + t.Errorf("Organization = %s, want 'Inference Ext'", x509Cert.Subject.Organization[0]) + } + }) + + t.Run("Validity Period", func(t *testing.T) { + now := time.Now() + if x509Cert.NotBefore.After(now) { + t.Error("Certificate NotBefore is in the future") + } + if x509Cert.NotAfter.Before(now) { + t.Error("Certificate NotAfter is in the past") + } + + // Check that certificate is valid for approximately 10 years + validityDuration := x509Cert.NotAfter.Sub(x509Cert.NotBefore) + expectedDuration := time.Hour * 24 * 365 * 10 // 10 years + tolerance := time.Hour * 24 // 1 day tolerance + + if validityDuration < expectedDuration-tolerance || validityDuration > expectedDuration+tolerance { + t.Errorf("Certificate validity duration = %v, want approximately %v", validityDuration, expectedDuration) + } + }) + + t.Run("Key Usage", func(t *testing.T) { + expectedKeyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + if x509Cert.KeyUsage != expectedKeyUsage { + t.Errorf("KeyUsage = %v, want %v", x509Cert.KeyUsage, expectedKeyUsage) + } + }) + + t.Run("Extended Key Usage", func(t *testing.T) { + if len(x509Cert.ExtKeyUsage) == 0 { + t.Fatal("No ExtKeyUsage found") + } + if x509Cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { + t.Errorf("ExtKeyUsage[0] = %v, want %v", x509Cert.ExtKeyUsage[0], x509.ExtKeyUsageServerAuth) + } + }) + + t.Run("Serial Number", func(t *testing.T) { + if x509Cert.SerialNumber == nil { + t.Error("Serial number is nil") + } + if x509Cert.SerialNumber.Sign() <= 0 { + t.Error("Serial number is not positive") + } + }) + + t.Run("Basic Constraints", func(t *testing.T) { + if !x509Cert.BasicConstraintsValid { + t.Error("BasicConstraintsValid is false") + } + }) +} + +func TestCreateSelfSignedTLSCertificate_PrivateKey(t *testing.T) { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + // Verify private key is present + if cert.PrivateKey == nil { + t.Fatal("Private key is nil") + } + + // Try to use the certificate for TLS + t.Run("TLS Config", func(t *testing.T) { + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + if len(config.Certificates) != 1 { + t.Errorf("TLS config has %d certificates, want 1", len(config.Certificates)) + } + }) +} + +func TestCreateSelfSignedTLSCertificate_Uniqueness(t *testing.T) { + // Generate two certificates and verify they are different + cert1, err1 := CreateSelfSignedTLSCertificate() + if err1 != nil { + t.Fatalf("First certificate generation failed: %v", err1) + } + + cert2, err2 := CreateSelfSignedTLSCertificate() + if err2 != nil { + t.Fatalf("Second certificate generation failed: %v", err2) + } + + // Parse both certificates + x509Cert1, err := x509.ParseCertificate(cert1.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse first certificate: %v", err) + } + + x509Cert2, err := x509.ParseCertificate(cert2.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse second certificate: %v", err) + } + + // Verify serial numbers are different + if x509Cert1.SerialNumber.Cmp(x509Cert2.SerialNumber) == 0 { + t.Error("Two certificates have the same serial number") + } + + // Verify certificates are different + if string(cert1.Certificate[0]) == string(cert2.Certificate[0]) { + t.Error("Two certificates are identical") + } +} + +func TestCreateSelfSignedTLSCertificate_SelfSigned(t *testing.T) { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse certificate: %v", err) + } + + // Verify certificate is self-signed by checking if issuer equals subject + if x509Cert.Issuer.String() != x509Cert.Subject.String() { + t.Errorf("Certificate is not self-signed: Issuer=%s, Subject=%s", + x509Cert.Issuer.String(), x509Cert.Subject.String()) + } + + // Verify certificate can be verified against itself + roots := x509.NewCertPool() + roots.AddCert(x509Cert) + + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + if _, err := x509Cert.Verify(opts); err != nil { + t.Errorf("Certificate failed self-verification: %v", err) + } +} + +func TestCreateSelfSignedTLSCertificate_KeySize(t *testing.T) { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse certificate: %v", err) + } + + // Verify RSA key size is 4096 bits + if rsaPubKey, ok := x509Cert.PublicKey.(interface{ Size() int }); ok { + keySize := rsaPubKey.Size() * 8 // Convert bytes to bits + if keySize != 4096 { + t.Errorf("RSA key size = %d bits, want 4096 bits", keySize) + } + } +} + +func TestCreateSelfSignedTLSCertificate_CertificateChain(t *testing.T) { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + // Verify certificate chain length + if len(cert.Certificate) != 1 { + t.Errorf("Certificate chain length = %d, want 1 (self-signed)", len(cert.Certificate)) + } +} + +func TestCreateSelfSignedTLSCertificate_Timestamps(t *testing.T) { + beforeCreation := time.Now().Add(-time.Minute) // 1 minute buffer + cert, err := CreateSelfSignedTLSCertificate() + afterCreation := time.Now().Add(time.Minute) // 1 minute buffer + + if err != nil { + t.Fatalf("CreateSelfSignedTLSCertificate() returned error: %v", err) + } + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse certificate: %v", err) + } + + // Verify NotBefore is around creation time + if x509Cert.NotBefore.Before(beforeCreation) || x509Cert.NotBefore.After(afterCreation) { + t.Errorf("NotBefore timestamp is out of expected range: %v", x509Cert.NotBefore) + } +} + +func TestCreateSelfSignedTLSCertificate_MultipleCalls(t *testing.T) { + // Test that multiple calls succeed + numCerts := 5 + certs := make([]tls.Certificate, numCerts) + serialNumbers := make(map[string]bool) + + for i := 0; i < numCerts; i++ { + cert, err := CreateSelfSignedTLSCertificate() + if err != nil { + t.Fatalf("Call %d failed: %v", i, err) + } + certs[i] = cert + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatalf("Failed to parse certificate %d: %v", i, err) + } + + serialNum := x509Cert.SerialNumber.String() + if serialNumbers[serialNum] { + t.Errorf("Duplicate serial number found: %s", serialNum) + } + serialNumbers[serialNum] = true + } + + if len(serialNumbers) != numCerts { + t.Errorf("Expected %d unique serial numbers, got %d", numCerts, len(serialNumbers)) + } +} + +func BenchmarkCreateSelfSignedTLSCertificate(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := CreateSelfSignedTLSCertificate() + if err != nil { + b.Fatalf("Certificate generation failed: %v", err) + } + } +} From a6d0bd244f976f37f798b3ccf083b6b83028eedd Mon Sep 17 00:00:00 2001 From: JaredforReal Date: Tue, 16 Dec 2025 00:32:51 +0800 Subject: [PATCH 2/2] try fix integration test dynamic config test install kubectl fail Signed-off-by: JaredforReal --- .../workflows/integration-test-dynamic-config.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/integration-test-dynamic-config.yml b/.github/workflows/integration-test-dynamic-config.yml index ed59291ad1..a5590de962 100644 --- a/.github/workflows/integration-test-dynamic-config.yml +++ b/.github/workflows/integration-test-dynamic-config.yml @@ -6,14 +6,14 @@ on: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" push: branches: - main paths-ignore: - - 'website/**' - - '**/*.md' + - "website/**" + - "**/*.md" workflow_dispatch: # Allow manual triggering concurrency: @@ -33,7 +33,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: "1.24" - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -57,7 +57,8 @@ jobs: - name: Install kubectl run: | - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) + curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" chmod +x kubectl sudo mv kubectl /usr/local/bin/kubectl @@ -174,5 +175,3 @@ jobs: if: always() run: | make e2e-cleanup || true - -