github.com/hernad/nomad@v1.6.112/nomad/scaling_endpoint_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package nomad
     5  
     6  import (
     7  	"testing"
     8  	"time"
     9  
    10  	msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
    11  	"github.com/hernad/nomad/acl"
    12  	"github.com/hernad/nomad/ci"
    13  	"github.com/hernad/nomad/helper/uuid"
    14  	"github.com/hernad/nomad/nomad/mock"
    15  	"github.com/hernad/nomad/nomad/structs"
    16  	"github.com/hernad/nomad/testutil"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestScalingEndpoint_StaleReadSupport(t *testing.T) {
    22  	ci.Parallel(t)
    23  	assert := assert.New(t)
    24  	list := &structs.ScalingPolicyListRequest{}
    25  	assert.True(list.IsRead())
    26  	get := &structs.ScalingPolicySpecificRequest{}
    27  	assert.True(get.IsRead())
    28  }
    29  
    30  func TestScalingEndpoint_GetPolicy(t *testing.T) {
    31  	ci.Parallel(t)
    32  	require := require.New(t)
    33  
    34  	s1, cleanupS1 := TestServer(t, nil)
    35  	defer cleanupS1()
    36  	codec := rpcClient(t, s1)
    37  	testutil.WaitForLeader(t, s1.RPC)
    38  
    39  	p1 := mock.ScalingPolicy()
    40  	p2 := mock.ScalingPolicy()
    41  	s1.fsm.State().UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2})
    42  
    43  	// Add another policy at a higher index.
    44  	p3 := mock.ScalingPolicy()
    45  	require.NoError(s1.fsm.State().UpsertScalingPolicies(2000, []*structs.ScalingPolicy{p3}))
    46  
    47  	// Lookup the first policy and perform assertions.
    48  	get := &structs.ScalingPolicySpecificRequest{
    49  		ID: p1.ID,
    50  		QueryOptions: structs.QueryOptions{
    51  			Region: "global",
    52  		},
    53  	}
    54  	var resp structs.SingleScalingPolicyResponse
    55  	err := msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp)
    56  	require.NoError(err)
    57  	require.EqualValues(1000, resp.Index)
    58  	require.Equal(*p1, *resp.Policy)
    59  
    60  	// Lookup non-existing policy
    61  	get.ID = uuid.Generate()
    62  	resp = structs.SingleScalingPolicyResponse{}
    63  	err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp)
    64  	require.NoError(err)
    65  	require.EqualValues(2000, resp.Index)
    66  	require.Nil(resp.Policy)
    67  }
    68  
    69  func TestScalingEndpoint_GetPolicy_ACL(t *testing.T) {
    70  	ci.Parallel(t)
    71  	require := require.New(t)
    72  
    73  	s1, root, cleanupS1 := TestACLServer(t, nil)
    74  	defer cleanupS1()
    75  	codec := rpcClient(t, s1)
    76  	testutil.WaitForLeader(t, s1.RPC)
    77  	state := s1.fsm.State()
    78  
    79  	p1 := mock.ScalingPolicy()
    80  	p2 := mock.ScalingPolicy()
    81  	state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2})
    82  
    83  	get := &structs.ScalingPolicySpecificRequest{
    84  		ID: p1.ID,
    85  		QueryOptions: structs.QueryOptions{
    86  			Region: "global",
    87  		},
    88  	}
    89  
    90  	// lookup without token should fail
    91  	var resp structs.SingleScalingPolicyResponse
    92  	err := msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp)
    93  	require.Error(err)
    94  	require.Contains(err.Error(), "Permission denied")
    95  
    96  	// Expect failure for request with an invalid token
    97  	invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid",
    98  		mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies}))
    99  	get.AuthToken = invalidToken.SecretID
   100  	err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp)
   101  	require.Error(err)
   102  	require.Contains(err.Error(), "Permission denied")
   103  	type testCase struct {
   104  		authToken string
   105  		name      string
   106  	}
   107  	cases := []testCase{
   108  		{
   109  			name:      "mgmt token should succeed",
   110  			authToken: root.SecretID,
   111  		},
   112  		{
   113  			name: "read disposition should succeed",
   114  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read",
   115  				mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)).SecretID,
   116  		},
   117  		{
   118  			name: "write disposition should succeed",
   119  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-write",
   120  				mock.NamespacePolicy(structs.DefaultNamespace, "write", nil)).SecretID,
   121  		},
   122  		{
   123  			name: "autoscaler disposition should succeed",
   124  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-autoscaler",
   125  				mock.NamespacePolicy(structs.DefaultNamespace, "scale", nil)).SecretID,
   126  		},
   127  		{
   128  			name: "list-jobs+read-job capability should succeed",
   129  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read-job-scaling",
   130  				mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs, acl.NamespaceCapabilityReadJob})).SecretID,
   131  		},
   132  	}
   133  
   134  	for _, tc := range cases {
   135  		get.AuthToken = tc.authToken
   136  		err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp)
   137  		require.NoError(err, tc.name)
   138  		require.EqualValues(1000, resp.Index)
   139  		require.NotNil(resp.Policy)
   140  	}
   141  
   142  }
   143  
   144  func TestScalingEndpoint_ListPolicies(t *testing.T) {
   145  	ci.Parallel(t)
   146  
   147  	s1, cleanupS1 := TestServer(t, nil)
   148  	defer cleanupS1()
   149  	codec := rpcClient(t, s1)
   150  	testutil.WaitForLeader(t, s1.RPC)
   151  
   152  	// Lookup the policies
   153  	var resp structs.ScalingPolicyListResponse
   154  	err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", &structs.ScalingPolicyListRequest{
   155  		QueryOptions: structs.QueryOptions{
   156  			Region:    "global",
   157  			Namespace: "default",
   158  		},
   159  	}, &resp)
   160  	require.NoError(t, err)
   161  	require.Empty(t, resp.Policies)
   162  
   163  	j1 := mock.Job()
   164  	j1polV := mock.ScalingPolicy()
   165  	j1polV.Type = "vertical-cpu"
   166  	j1polV.TargetTask(j1, j1.TaskGroups[0], j1.TaskGroups[0].Tasks[0])
   167  	j1polH := mock.ScalingPolicy()
   168  	j1polH.Type = "horizontal"
   169  	j1polH.TargetTaskGroup(j1, j1.TaskGroups[0])
   170  
   171  	j2 := mock.Job()
   172  	j2polH := mock.ScalingPolicy()
   173  	j2polH.Type = "horizontal"
   174  	j2polH.TargetTaskGroup(j2, j2.TaskGroups[0])
   175  
   176  	s1.fsm.State().UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j1)
   177  	s1.fsm.State().UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j2)
   178  
   179  	pols := []*structs.ScalingPolicy{j1polV, j1polH, j2polH}
   180  	s1.fsm.State().UpsertScalingPolicies(1000, pols)
   181  	for _, p := range pols {
   182  		p.ModifyIndex = 1000
   183  		p.CreateIndex = 1000
   184  	}
   185  
   186  	cases := []struct {
   187  		Label    string
   188  		Job      string
   189  		Type     string
   190  		Expected []*structs.ScalingPolicy
   191  	}{
   192  		{
   193  			Label:    "all policies",
   194  			Expected: []*structs.ScalingPolicy{j1polH, j1polV, j2polH},
   195  		},
   196  		{
   197  			Label:    "job filter",
   198  			Job:      j1.ID,
   199  			Expected: []*structs.ScalingPolicy{j1polH, j1polV},
   200  		},
   201  		{
   202  			Label:    "type filter",
   203  			Type:     "horizontal",
   204  			Expected: []*structs.ScalingPolicy{j1polH, j2polH},
   205  		},
   206  		{
   207  			Label:    "job and type",
   208  			Job:      j1.ID,
   209  			Type:     "horizontal",
   210  			Expected: []*structs.ScalingPolicy{j1polH},
   211  		},
   212  	}
   213  
   214  	for _, tc := range cases {
   215  		t.Run(tc.Label, func(t *testing.T) {
   216  			get := &structs.ScalingPolicyListRequest{
   217  				Job:  tc.Job,
   218  				Type: tc.Type,
   219  				QueryOptions: structs.QueryOptions{
   220  					Region:    "global",
   221  					Namespace: "default",
   222  				},
   223  			}
   224  			var resp structs.ScalingPolicyListResponse
   225  			err = msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp)
   226  			require.NoError(t, err)
   227  			stubs := []*structs.ScalingPolicyListStub{}
   228  			for _, p := range tc.Expected {
   229  				stubs = append(stubs, p.Stub())
   230  			}
   231  			require.ElementsMatch(t, stubs, resp.Policies)
   232  		})
   233  	}
   234  }
   235  
   236  func TestScalingEndpoint_ListPolicies_ACL(t *testing.T) {
   237  	ci.Parallel(t)
   238  	require := require.New(t)
   239  
   240  	s1, root, cleanupS1 := TestACLServer(t, nil)
   241  	defer cleanupS1()
   242  	codec := rpcClient(t, s1)
   243  	testutil.WaitForLeader(t, s1.RPC)
   244  	state := s1.fsm.State()
   245  
   246  	p1 := mock.ScalingPolicy()
   247  	p2 := mock.ScalingPolicy()
   248  	state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2})
   249  
   250  	get := &structs.ScalingPolicyListRequest{
   251  		QueryOptions: structs.QueryOptions{
   252  			Region:    "global",
   253  			Namespace: "default",
   254  		},
   255  	}
   256  
   257  	// lookup without token should fail
   258  	var resp structs.ACLPolicyListResponse
   259  	err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp)
   260  	require.Error(err)
   261  	require.Contains(err.Error(), "Permission denied")
   262  
   263  	// Expect failure for request with an invalid token
   264  	invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid",
   265  		mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies}))
   266  	get.AuthToken = invalidToken.SecretID
   267  	require.Error(err)
   268  	require.Contains(err.Error(), "Permission denied")
   269  
   270  	type testCase struct {
   271  		authToken string
   272  		name      string
   273  	}
   274  	cases := []testCase{
   275  		{
   276  			name:      "mgmt token should succeed",
   277  			authToken: root.SecretID,
   278  		},
   279  		{
   280  			name: "read disposition should succeed",
   281  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read",
   282  				mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)).SecretID,
   283  		},
   284  		{
   285  			name: "write disposition should succeed",
   286  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-write",
   287  				mock.NamespacePolicy(structs.DefaultNamespace, "write", nil)).SecretID,
   288  		},
   289  		{
   290  			name: "autoscaler disposition should succeed",
   291  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-autoscaler",
   292  				mock.NamespacePolicy(structs.DefaultNamespace, "scale", nil)).SecretID,
   293  		},
   294  		{
   295  			name: "list-scaling-policies capability should succeed",
   296  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-list-scaling-policies",
   297  				mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies})).SecretID,
   298  		},
   299  		{
   300  			name: "list-jobs+read-job capability should succeed",
   301  			authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read-job-scaling",
   302  				mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs, acl.NamespaceCapabilityReadJob})).SecretID,
   303  		},
   304  	}
   305  
   306  	for _, tc := range cases {
   307  		get.AuthToken = tc.authToken
   308  		err = msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp)
   309  		require.NoError(err, tc.name)
   310  		require.EqualValues(1000, resp.Index)
   311  		require.Len(resp.Policies, 2)
   312  	}
   313  }
   314  
   315  func TestScalingEndpoint_ListPolicies_Blocking(t *testing.T) {
   316  	ci.Parallel(t)
   317  	require := require.New(t)
   318  
   319  	s1, cleanupS1 := TestServer(t, nil)
   320  	defer cleanupS1()
   321  	state := s1.fsm.State()
   322  	codec := rpcClient(t, s1)
   323  	testutil.WaitForLeader(t, s1.RPC)
   324  
   325  	// Create the policies
   326  	p1 := mock.ScalingPolicy()
   327  	p2 := mock.ScalingPolicy()
   328  
   329  	// First create an unrelated policy
   330  	time.AfterFunc(100*time.Millisecond, func() {
   331  		err := state.UpsertScalingPolicies(100, []*structs.ScalingPolicy{p1})
   332  		require.NoError(err)
   333  	})
   334  
   335  	// Upsert the policy we are watching later
   336  	time.AfterFunc(200*time.Millisecond, func() {
   337  		err := state.UpsertScalingPolicies(200, []*structs.ScalingPolicy{p2})
   338  		require.NoError(err)
   339  	})
   340  
   341  	// Lookup the policy
   342  	req := &structs.ScalingPolicyListRequest{
   343  		QueryOptions: structs.QueryOptions{
   344  			Region:        "global",
   345  			Namespace:     "default",
   346  			MinQueryIndex: 150,
   347  		},
   348  	}
   349  	var resp structs.ScalingPolicyListResponse
   350  	start := time.Now()
   351  	err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", req, &resp)
   352  	require.NoError(err)
   353  
   354  	require.True(time.Since(start) > 200*time.Millisecond, "should block: %#v", resp)
   355  	require.EqualValues(200, resp.Index, "bad index")
   356  	require.Len(resp.Policies, 2)
   357  	require.ElementsMatch([]string{p1.ID, p2.ID}, []string{resp.Policies[0].ID, resp.Policies[1].ID})
   358  }