agones.dev/agones@v1.53.0/pkg/fleetautoscalers/fleetautoscalerwasm_test.go (about)

     1  /*
     2   * Copyright 2025 Google LLC All Rights Reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package fleetautoscalers
    18  
    19  import (
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/hex"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"os"
    26  	"path/filepath"
    27  	"testing"
    28  
    29  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    30  	autoscalingv1 "agones.dev/agones/pkg/apis/autoscaling/v1"
    31  	utilruntime "agones.dev/agones/pkg/util/runtime"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  // defaultWasmFixtures creates default fixtures for testing WasmPolicy
    37  func defaultWasmFixtures() (*autoscalingv1.FleetAutoscaler, *agonesv1.Fleet) {
    38  	fas, f := defaultFixtures()
    39  	fas.Spec.Policy.Type = autoscalingv1.WasmPolicyType
    40  	fas.Spec.Policy.Buffer = nil
    41  
    42  	// Set up WasmPolicy
    43  	url := "plugin.wasm"
    44  	fas.Spec.Policy.Wasm = &autoscalingv1.WasmPolicy{
    45  		Function: "scale",
    46  		Config: map[string]string{
    47  			"buffer_size": "5",
    48  		},
    49  		From: autoscalingv1.WasmFrom{
    50  			URL: &autoscalingv1.URLConfiguration{
    51  				URL: &url,
    52  			},
    53  		},
    54  	}
    55  
    56  	return fas, f
    57  }
    58  
    59  func TestApplyWasmPolicy(t *testing.T) {
    60  	t.Parallel()
    61  
    62  	// Enable the WASM autoscaler feature flag for testing
    63  	utilruntime.FeatureTestMutex.Lock()
    64  	defer utilruntime.FeatureTestMutex.Unlock()
    65  	utilruntime.EnableAllFeatures()
    66  
    67  	// Find the WASM plugin file
    68  	wasmFilePath, err := filepath.Abs(filepath.Join("..", "..", "examples", "autoscaler-wasm"))
    69  	require.NoError(t, err)
    70  
    71  	_, err = os.Stat(wasmFilePath)
    72  	require.NoError(t, err, "WASM plugin file not found at %s", wasmFilePath)
    73  
    74  	// Create a test server to serve the WASM plugin
    75  	svrDir := http.Dir(wasmFilePath)
    76  	sourcePluginPath := filepath.Join(wasmFilePath, "plugin.wasm")
    77  	// Compute the SHA256 hash of the plugin for Hash tests
    78  	pluginBytes, err := os.ReadFile(sourcePluginPath)
    79  	require.NoError(t, err)
    80  	sum := sha256.Sum256(pluginBytes)
    81  	hashStr := hex.EncodeToString(sum[:])
    82  	// Create an incorrect hash (same length, wrong value) for negative test
    83  	badSum := make([]byte, len(sum))
    84  	copy(badSum, sum[:])
    85  	badSum[0] ^= 0xFF // flip first byte to ensure mismatch
    86  	badHash := hex.EncodeToString(badSum)
    87  
    88  	sourceFS := http.FileServer(svrDir)
    89  	srv := httptest.NewServer(sourceFS)
    90  	defer srv.Close()
    91  
    92  	// Create test fixtures
    93  	fas, f := defaultWasmFixtures()
    94  
    95  	// Update the URL to point to our test server
    96  	fileURL := srv.URL + "/plugin.wasm"
    97  	fas.Spec.Policy.Wasm.From.URL.URL = &fileURL
    98  
    99  	// Create a logger for testing
   100  	logger := &FasLogger{
   101  		fas:        fas,
   102  		baseLogger: newTestLogger(),
   103  	}
   104  
   105  	type expected struct {
   106  		replicas int32
   107  		limited  bool
   108  		err      string
   109  	}
   110  
   111  	type testCase struct {
   112  		wasmPolicy              *autoscalingv1.WasmPolicy
   113  		fleet                   *agonesv1.Fleet
   114  		specReplicas            int32
   115  		statusReplicas          int32
   116  		statusAllocatedReplicas int32
   117  		statusReadyReplicas     int32
   118  		expected                expected
   119  	}
   120  
   121  	var testCases = map[string]testCase{
   122  		"Correct Hash provided (sha256), scale up needed": {
   123  			wasmPolicy: &autoscalingv1.WasmPolicy{
   124  				Function: "scale",
   125  				Config: map[string]string{
   126  					"buffer_size": "5",
   127  				},
   128  				From: autoscalingv1.WasmFrom{
   129  					URL: &autoscalingv1.URLConfiguration{
   130  						URL: &fileURL,
   131  					},
   132  				},
   133  				Hash: hashStr,
   134  			},
   135  			fleet:                   f,
   136  			specReplicas:            10,
   137  			statusReplicas:          10,
   138  			statusAllocatedReplicas: 8,
   139  			statusReadyReplicas:     2,
   140  			expected: expected{
   141  				replicas: 13,
   142  				limited:  false,
   143  				err:      "",
   144  			},
   145  		},
   146  		"Incorrect Hash provided (sha256), plugin creation fails": {
   147  			wasmPolicy: &autoscalingv1.WasmPolicy{
   148  				Function: "scale",
   149  				Config: map[string]string{
   150  					"buffer_size": "5",
   151  				},
   152  				From: autoscalingv1.WasmFrom{
   153  					URL: &autoscalingv1.URLConfiguration{
   154  						URL: &fileURL,
   155  					},
   156  				},
   157  				Hash: badHash,
   158  			},
   159  			fleet: f,
   160  			expected: expected{
   161  				replicas: 0,
   162  				limited:  false,
   163  				err:      "hash mismatch for module",
   164  			},
   165  		},
   166  		"Default buffer size (5), scale up needed": {
   167  			wasmPolicy: &autoscalingv1.WasmPolicy{
   168  				Function: "scale",
   169  				Config: map[string]string{
   170  					"buffer_size": "5",
   171  				},
   172  				From: autoscalingv1.WasmFrom{
   173  					URL: &autoscalingv1.URLConfiguration{
   174  						URL: &fileURL,
   175  					},
   176  				},
   177  			},
   178  			fleet:                   f,
   179  			specReplicas:            10,
   180  			statusReplicas:          10,
   181  			statusAllocatedReplicas: 8,
   182  			statusReadyReplicas:     2,
   183  			expected: expected{
   184  				replicas: 13, // allocated (8) + buffer (5)
   185  				limited:  false,
   186  				err:      "",
   187  			},
   188  		},
   189  		"Default buffer size (5), no scaling needed": {
   190  			wasmPolicy: &autoscalingv1.WasmPolicy{
   191  				Function: "scale",
   192  				Config: map[string]string{
   193  					"buffer_size": "5",
   194  				},
   195  				From: autoscalingv1.WasmFrom{
   196  					URL: &autoscalingv1.URLConfiguration{
   197  						URL: &fileURL,
   198  					},
   199  				},
   200  			},
   201  			fleet:                   f,
   202  			specReplicas:            15,
   203  			statusReplicas:          15,
   204  			statusAllocatedReplicas: 10,
   205  			statusReadyReplicas:     5,
   206  			expected: expected{
   207  				replicas: 15, // already at the right size
   208  				limited:  false,
   209  				err:      "",
   210  			},
   211  		},
   212  		"Custom buffer size (10), scale up needed": {
   213  			wasmPolicy: &autoscalingv1.WasmPolicy{
   214  				Function: "scale",
   215  				Config: map[string]string{
   216  					"buffer_size": "10",
   217  				},
   218  				From: autoscalingv1.WasmFrom{
   219  					URL: &autoscalingv1.URLConfiguration{
   220  						URL: &fileURL,
   221  					},
   222  				},
   223  			},
   224  			fleet:                   f,
   225  			specReplicas:            15,
   226  			statusReplicas:          15,
   227  			statusAllocatedReplicas: 10,
   228  			statusReadyReplicas:     5,
   229  			expected: expected{
   230  				replicas: 20, // allocated (10) + buffer (10)
   231  				limited:  false,
   232  				err:      "",
   233  			},
   234  		},
   235  		"nil WasmPolicy, error returned": {
   236  			wasmPolicy: nil,
   237  			fleet:      f,
   238  			expected: expected{
   239  				replicas: 0,
   240  				limited:  false,
   241  				err:      "wasmPolicy parameter must not be nil",
   242  			},
   243  		},
   244  		"nil Fleet, error returned": {
   245  			wasmPolicy: &autoscalingv1.WasmPolicy{
   246  				Function: "scale",
   247  				Config: map[string]string{
   248  					"buffer_size": "5",
   249  				},
   250  				From: autoscalingv1.WasmFrom{
   251  					URL: &autoscalingv1.URLConfiguration{
   252  						URL: &fileURL,
   253  					},
   254  				},
   255  			},
   256  			fleet: nil,
   257  			expected: expected{
   258  				replicas: 0,
   259  				limited:  false,
   260  				err:      "fleet parameter must not be nil",
   261  			},
   262  		},
   263  		"Invalid URL in WasmPolicy": {
   264  			wasmPolicy: &autoscalingv1.WasmPolicy{
   265  				Function: "scale",
   266  				Config: map[string]string{
   267  					"buffer_size": "5",
   268  				},
   269  				From: autoscalingv1.WasmFrom{
   270  					URL: &autoscalingv1.URLConfiguration{
   271  						URL: nil,
   272  					},
   273  				},
   274  			},
   275  			fleet: f,
   276  			expected: expected{
   277  				replicas: 0,
   278  				limited:  false,
   279  				err:      "service was not provided, either URL or Service must be provided",
   280  			},
   281  		},
   282  		"Function set to scaleNone, no scaling occurs": {
   283  			wasmPolicy: &autoscalingv1.WasmPolicy{
   284  				Function: "scaleNone",
   285  				From: autoscalingv1.WasmFrom{
   286  					URL: &autoscalingv1.URLConfiguration{
   287  						URL: &fileURL,
   288  					},
   289  				},
   290  			},
   291  			fleet:                   f,
   292  			specReplicas:            10,
   293  			statusReplicas:          10,
   294  			statusAllocatedReplicas: 8,
   295  			statusReadyReplicas:     2,
   296  			expected: expected{
   297  				replicas: 10,
   298  				limited:  false,
   299  				err:      "",
   300  			},
   301  		},
   302  	}
   303  
   304  	for name, tc := range testCases {
   305  		t.Run(name, func(t *testing.T) {
   306  
   307  			var fleet *agonesv1.Fleet
   308  			if tc.fleet != nil {
   309  				fleet = tc.fleet.DeepCopy()
   310  				fleet.Spec.Replicas = tc.specReplicas
   311  				fleet.Status.Replicas = tc.statusReplicas
   312  				fleet.Status.AllocatedReplicas = tc.statusAllocatedReplicas
   313  				fleet.Status.ReadyReplicas = tc.statusReadyReplicas
   314  			}
   315  
   316  			// Create a new state map for each test case
   317  			state := make(map[string]any)
   318  
   319  			replicas, limited, err := applyWasmPolicy(context.Background(), state, tc.wasmPolicy, fleet, logger)
   320  
   321  			if tc.expected.err != "" {
   322  				require.ErrorContains(t, err, tc.expected.err)
   323  			} else {
   324  				require.NoError(t, err)
   325  				assert.Equal(t, tc.expected.replicas, replicas)
   326  				assert.Equal(t, tc.expected.limited, limited)
   327  			}
   328  		})
   329  	}
   330  }