// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package agent import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/raft" autopilot "github.com/hashicorp/raft-autopilot" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" ) func TestOperator_RaftConfiguration(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, "") defer a.Shutdown() body := bytes.NewBuffer(nil) req, _ := http.NewRequest("GET", "/v1/operator/raft/configuration", body) resp := httptest.NewRecorder() obj, err := a.srv.OperatorRaftConfiguration(resp, req) if err != nil { t.Fatalf("err: %v", err) } if resp.Code != 200 { t.Fatalf("bad code: %d", resp.Code) } out, ok := obj.(structs.RaftConfigurationResponse) if !ok { t.Fatalf("unexpected: %T", obj) } if len(out.Servers) != 1 || !out.Servers[0].Leader || !out.Servers[0].Voter { t.Fatalf("bad: %v", out) } } func TestOperator_RaftPeer(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() t.Run("", func(t *testing.T) { a := NewTestAgent(t, "") defer a.Shutdown() body := bytes.NewBuffer(nil) req, _ := http.NewRequest("DELETE", "/v1/operator/raft/peer?address=nope", body) // If we get this error, it proves we sent the address all the // way through. resp := httptest.NewRecorder() _, err := a.srv.OperatorRaftPeer(resp, req) if err == nil || !strings.Contains(err.Error(), "address \"nope\" was not found in the Raft configuration") { t.Fatalf("err: %v", err) } }) t.Run("", func(t *testing.T) { a := NewTestAgent(t, "") defer a.Shutdown() body := bytes.NewBuffer(nil) req, _ := http.NewRequest("DELETE", "/v1/operator/raft/peer?id=nope", body) // If we get this error, it proves we sent the ID all the // way through. resp := httptest.NewRecorder() _, err := a.srv.OperatorRaftPeer(resp, req) if err == nil || !strings.Contains(err.Error(), "id \"nope\" was not found in the Raft configuration") { t.Fatalf("err: %v", err) } }) } func TestOperator_KeyringInstall(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() oldKey := "H3/9gBxcKKRf45CaI2DlRg==" newKey := "z90lFx3sZZLtTOkutXcwYg==" a := NewTestAgent(t, ` encrypt = "`+oldKey+`" `) defer a.Shutdown() body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) req, _ := http.NewRequest("POST", "/v1/operator/keyring", body) resp := httptest.NewRecorder() _, err := a.srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %s", err) } listResponse, err := a.ListKeys("", false, 0) if err != nil { t.Fatalf("err: %s", err) } if len(listResponse.Responses) != 2 { t.Fatalf("bad: %d", len(listResponse.Responses)) } for _, response := range listResponse.Responses { count, ok := response.Keys[newKey] if !ok { t.Fatalf("bad: %v", response.Keys) } if count != response.NumNodes { t.Fatalf("bad: %d, %d", count, response.NumNodes) } } } func TestOperator_KeyringList(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() key := "H3/9gBxcKKRf45CaI2DlRg==" a := NewTestAgent(t, ` encrypt = "`+key+`" `) defer a.Shutdown() req, _ := http.NewRequest("GET", "/v1/operator/keyring", nil) resp := httptest.NewRecorder() r, err := a.srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %v", err) } responses, ok := r.([]*structs.KeyringResponse) if !ok { t.Fatalf("err: %v", !ok) } // Check that we get both a LAN and WAN response, and that they both only // contain the original key if len(responses) != 2 { t.Fatalf("bad: %d", len(responses)) } // WAN if len(responses[0].Keys) != 1 { t.Fatalf("bad: %d", len(responses[0].Keys)) } if !responses[0].WAN { t.Fatalf("bad: %v", responses[0].WAN) } if _, ok := responses[0].Keys[key]; !ok { t.Fatalf("bad: %v", ok) } // LAN if len(responses[1].Keys) != 1 { t.Fatalf("bad: %d", len(responses[1].Keys)) } if responses[1].WAN { t.Fatalf("bad: %v", responses[1].WAN) } if _, ok := responses[1].Keys[key]; !ok { t.Fatalf("bad: %v", ok) } } func TestOperator_KeyringListLocalOnly(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() key := "H3/9gBxcKKRf45CaI2DlRg==" a := NewTestAgent(t, ` encrypt = "`+key+`" `) defer a.Shutdown() req, _ := http.NewRequest("GET", "/v1/operator/keyring?local-only=1", nil) resp := httptest.NewRecorder() r, err := a.srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %v", err) } responses, ok := r.([]*structs.KeyringResponse) if !ok { t.Fatalf("err: %v", !ok) } // Check that we only get a LAN response with the original key if len(responses) != 1 { for _, r := range responses { fmt.Println(r) } t.Fatalf("bad: %d", len(responses)) } // LAN if len(responses[0].Keys) != 1 { t.Fatalf("bad: %d", len(responses[1].Keys)) } if responses[0].WAN { t.Fatalf("bad: %v", responses[1].WAN) } if _, ok := responses[0].Keys[key]; !ok { t.Fatalf("bad: %v", ok) } } func TestOperator_KeyringRemove(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() key := "H3/9gBxcKKRf45CaI2DlRg==" tempKey := "z90lFx3sZZLtTOkutXcwYg==" a := NewTestAgent(t, ` encrypt = "`+key+`" `) defer a.Shutdown() _, err := a.InstallKey(tempKey, "", 0) if err != nil { t.Fatalf("err: %v", err) } // Make sure the temp key is installed list, err := a.ListKeys("", false, 0) if err != nil { t.Fatalf("err: %v", err) } responses := list.Responses if len(responses) != 2 { t.Fatalf("bad: %d", len(responses)) } for _, response := range responses { if len(response.Keys) != 2 { t.Fatalf("bad: %d", len(response.Keys)) } if _, ok := response.Keys[tempKey]; !ok { t.Fatalf("bad: %v", ok) } } body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", tempKey)) req, _ := http.NewRequest("DELETE", "/v1/operator/keyring", body) resp := httptest.NewRecorder() if _, err := a.srv.OperatorKeyringEndpoint(resp, req); err != nil { t.Fatalf("err: %s", err) } // Make sure the temp key has been removed list, err = a.ListKeys("", false, 0) if err != nil { t.Fatalf("err: %v", err) } responses = list.Responses if len(responses) != 2 { t.Fatalf("bad: %d", len(responses)) } for _, response := range responses { if len(response.Keys) != 1 { t.Fatalf("bad: %d", len(response.Keys)) } if _, ok := response.Keys[tempKey]; ok { t.Fatalf("bad: %v", ok) } } } func TestOperator_KeyringUse(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() oldKey := "H3/9gBxcKKRf45CaI2DlRg==" newKey := "z90lFx3sZZLtTOkutXcwYg==" a := NewTestAgent(t, ` encrypt = "`+oldKey+`" `) defer a.Shutdown() if _, err := a.InstallKey(newKey, "", 0); err != nil { t.Fatalf("err: %v", err) } body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) req, _ := http.NewRequest("PUT", "/v1/operator/keyring", body) resp := httptest.NewRecorder() _, err := a.srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %s", err) } if _, err := a.RemoveKey(oldKey, "", 0); err != nil { t.Fatalf("err: %v", err) } // Make sure only the new key remains list, err := a.ListKeys("", false, 0) if err != nil { t.Fatalf("err: %v", err) } responses := list.Responses if len(responses) != 2 { t.Fatalf("bad: %d", len(responses)) } for _, response := range responses { if len(response.Keys) != 1 { t.Fatalf("bad: %d", len(response.Keys)) } if _, ok := response.Keys[newKey]; !ok { t.Fatalf("bad: %v", ok) } } } func TestOperator_Keyring_InvalidRelayFactor(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() key := "H3/9gBxcKKRf45CaI2DlRg==" a := NewTestAgent(t, ` encrypt = "`+key+`" `) defer a.Shutdown() cases := map[string]string{ "999": "Relay factor must be in range", "asdf": "Error parsing relay factor", } for relayFactor, errString := range cases { req, err := http.NewRequest("GET", "/v1/operator/keyring?relay-factor="+relayFactor, nil) require.NoError(t, err) resp := httptest.NewRecorder() _, err = a.srv.OperatorKeyringEndpoint(resp, req) require.Error(t, err, "tc: "+relayFactor) require.Contains(t, err.Error(), errString, "tc: "+relayFactor) } } func TestOperator_Keyring_LocalOnly(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() key := "H3/9gBxcKKRf45CaI2DlRg==" a := NewTestAgent(t, ` encrypt = "`+key+`" `) defer a.Shutdown() cases := []struct { description string method string local interface{} ok bool }{ {"all ok", "GET", true, true}, {"garbage local-only value", "GET", "garbage", false}, {"wrong method (DELETE)", "DELETE", true, false}, } for _, tc := range cases { url := fmt.Sprintf("/v1/operator/keyring?local-only=%v", tc.local) req, err := http.NewRequest(tc.method, url, nil) require.NoError(t, err, "tc: "+tc.description) resp := httptest.NewRecorder() _, err = a.srv.OperatorKeyringEndpoint(resp, req) if tc.ok { require.NoError(t, err, "tc: "+tc.description) } if !tc.ok { require.Error(t, err, "tc: "+tc.description) } } } func TestOperator_AutopilotGetConfiguration(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, "") defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") body := bytes.NewBuffer(nil) req, _ := http.NewRequest("GET", "/v1/operator/autopilot/configuration", body) resp := httptest.NewRecorder() obj, err := a.srv.OperatorAutopilotConfiguration(resp, req) if err != nil { t.Fatalf("err: %v", err) } if resp.Code != 200 { t.Fatalf("bad code: %d", resp.Code) } out, ok := obj.(api.AutopilotConfiguration) if !ok { t.Fatalf("unexpected: %T", obj) } if !out.CleanupDeadServers { t.Fatalf("bad: %#v", out) } } func TestOperator_AutopilotSetConfiguration(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, "") defer a.Shutdown() // Provide a non-default value only for CleanupDeadServers. // Expect all other fields to be updated with default values // (except CreateIndex and ModifyIndex). body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`)) expected := structs.AutopilotConfig{ CleanupDeadServers: false, // only non-default value LastContactThreshold: 200 * time.Millisecond, MaxTrailingLogs: 250, MinQuorum: 0, ServerStabilizationTime: 10 * time.Second, RedundancyZoneTag: "", DisableUpgradeMigration: false, UpgradeVersionTag: "", } req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body) resp := httptest.NewRecorder() if _, err := a.srv.OperatorAutopilotConfiguration(resp, req); err != nil { t.Fatalf("err: %v", err) } if resp.Code != 200 { t.Fatalf("bad code: %d", resp.Code) } args := structs.DCSpecificRequest{ Datacenter: "dc1", } var reply structs.AutopilotConfig if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil { t.Fatalf("err: %v", err) } // For equality comparison check, ignore CreateIndex and ModifyIndex expected.CreateIndex = reply.CreateIndex expected.ModifyIndex = reply.ModifyIndex require.Equal(t, expected, reply) } func TestOperator_AutopilotCASConfiguration(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, "") defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`)) req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body) resp := httptest.NewRecorder() if _, err := a.srv.OperatorAutopilotConfiguration(resp, req); err != nil { t.Fatalf("err: %v", err) } if resp.Code != 200 { t.Fatalf("bad code: %d", resp.Code) } args := structs.DCSpecificRequest{ Datacenter: "dc1", } var reply structs.AutopilotConfig if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil { t.Fatalf("err: %v", err) } if reply.CleanupDeadServers { t.Fatalf("bad: %#v", reply) } // Create a CAS request, bad index { buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`)) req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex-1), buf) resp := httptest.NewRecorder() obj, err := a.srv.OperatorAutopilotConfiguration(resp, req) if err != nil { t.Fatalf("err: %v", err) } if res := obj.(bool); res { t.Fatalf("should NOT work") } } // Create a CAS request, good index { buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`)) req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex), buf) resp := httptest.NewRecorder() obj, err := a.srv.OperatorAutopilotConfiguration(resp, req) if err != nil { t.Fatalf("err: %v", err) } if res := obj.(bool); !res { t.Fatalf("should work") } } // Verify the update if err := a.RPC(context.Background(), "Operator.AutopilotGetConfiguration", &args, &reply); err != nil { t.Fatalf("err: %v", err) } if !reply.CleanupDeadServers { t.Fatalf("bad: %#v", reply) } } func TestOperator_ServerHealth(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, ` raft_protocol = 3 `) defer a.Shutdown() body := bytes.NewBuffer(nil) req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body) retry.Run(t, func(r *retry.R) { resp := httptest.NewRecorder() obj, err := a.srv.OperatorServerHealth(resp, req) if err != nil { r.Fatalf("err: %v", err) } if resp.Code != 200 { r.Fatalf("bad code: %d", resp.Code) } out, ok := obj.(*api.OperatorHealthReply) if !ok { r.Fatalf("unexpected: %T", obj) } if len(out.Servers) != 1 || !out.Servers[0].Healthy || out.Servers[0].Name != a.Config.NodeName || out.Servers[0].SerfStatus != "alive" || out.FailureTolerance != 0 { r.Fatalf("bad: %v", out) } }) } func TestOperator_ServerHealth_Unhealthy(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() a := NewTestAgent(t, ` raft_protocol = 3 autopilot { last_contact_threshold = "-1s" } `) defer a.Shutdown() body := bytes.NewBuffer(nil) req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body) retry.Run(t, func(r *retry.R) { resp := httptest.NewRecorder() obj, err := a.srv.OperatorServerHealth(resp, req) if err != nil { r.Fatalf("err: %v", err) } if resp.Code != 429 { r.Fatalf("bad code: %d", resp.Code) } out, ok := obj.(*api.OperatorHealthReply) if !ok { r.Fatalf("unexpected: %T", obj) } if len(out.Servers) != 1 || out.Healthy || out.Servers[0].Name != a.Config.NodeName { r.Fatalf("bad: %#v", out.Servers) } }) } func TestOperator_AutopilotState(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } a := NewTestAgent(t, "") defer a.Shutdown() req, err := http.NewRequest("GET", "/v1/operator/autopilot/state", nil) require.NoError(t, err) retry.Run(t, func(r *retry.R) { resp := httptest.NewRecorder() obj, err := a.srv.OperatorAutopilotState(resp, req) require.NoError(r, err) require.Equal(r, 200, resp.Code) state, ok := obj.(*api.AutopilotState) require.True(r, ok) srv, ok := state.Servers[string(a.config.NodeID)] require.True(r, ok) require.True(r, srv.Healthy) require.Equal(r, a.config.NodeName, srv.Name) }) } func TestAutopilotStateToAPIConversion(t *testing.T) { var leaderID raft.ServerID = "79324811-9588-4311-b208-f272e38aaabf" var follower1ID raft.ServerID = "ef8aee9a-f9d6-4ec4-b383-aac956bdb80f" var follower2ID raft.ServerID = "ae84aefb-a303-4734-8739-5c102d4ee2d9" input := autopilot.State{ Healthy: true, FailureTolerance: 1, Leader: leaderID, Voters: []raft.ServerID{ leaderID, follower1ID, follower2ID, }, Servers: map[raft.ServerID]*autopilot.ServerState{ leaderID: { Server: autopilot.Server{ ID: leaderID, Name: "node1", Address: "198.18.0.1:8300", NodeStatus: autopilot.NodeAlive, Version: "1.9.0", Meta: map[string]string{ "foo": "bar", }, NodeType: autopilot.NodeVoter, }, State: autopilot.RaftLeader, Stats: autopilot.ServerStats{ LastContact: 0, LastTerm: 3, LastIndex: 42, }, Health: autopilot.ServerHealth{ Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 51, 0, 0, time.UTC), }, }, follower1ID: { Server: autopilot.Server{ ID: follower1ID, Name: "node2", Address: "198.18.0.2:8300", NodeStatus: autopilot.NodeAlive, Version: "1.9.0", Meta: map[string]string{ "bar": "baz", }, NodeType: autopilot.NodeVoter, }, State: autopilot.RaftVoter, Stats: autopilot.ServerStats{ LastContact: time.Millisecond, LastTerm: 3, LastIndex: 41, }, Health: autopilot.ServerHealth{ Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 52, 0, 0, time.UTC), }, }, follower2ID: { Server: autopilot.Server{ ID: follower2ID, Name: "node3", Address: "198.18.0.3:8300", NodeStatus: autopilot.NodeAlive, Version: "1.9.0", Meta: map[string]string{ "baz": "foo", }, NodeType: autopilot.NodeVoter, }, State: autopilot.RaftVoter, Stats: autopilot.ServerStats{ LastContact: 2 * time.Millisecond, LastTerm: 3, LastIndex: 39, }, Health: autopilot.ServerHealth{ Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 53, 0, 0, time.UTC), }, }, }, } expected := api.AutopilotState{ Healthy: true, FailureTolerance: 1, OptimisticFailureTolerance: 1, Leader: string(leaderID), Voters: []string{ string(leaderID), string(follower1ID), string(follower2ID), }, Servers: map[string]api.AutopilotServer{ string(leaderID): { ID: string(leaderID), Name: "node1", Address: "198.18.0.1:8300", NodeStatus: "alive", Version: "1.9.0", Meta: map[string]string{ "foo": "bar", }, NodeType: api.AutopilotTypeVoter, Status: api.AutopilotServerLeader, LastContact: api.NewReadableDuration(0), LastTerm: 3, LastIndex: 42, Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 51, 0, 0, time.UTC), }, string(follower1ID): { ID: string(follower1ID), Name: "node2", Address: "198.18.0.2:8300", NodeStatus: "alive", Version: "1.9.0", Meta: map[string]string{ "bar": "baz", }, NodeType: api.AutopilotTypeVoter, Status: api.AutopilotServerVoter, LastContact: api.NewReadableDuration(time.Millisecond), LastTerm: 3, LastIndex: 41, Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 52, 0, 0, time.UTC), }, string(follower2ID): { ID: string(follower2ID), Name: "node3", Address: "198.18.0.3:8300", NodeStatus: "alive", Version: "1.9.0", Meta: map[string]string{ "baz": "foo", }, NodeType: api.AutopilotTypeVoter, Status: api.AutopilotServerVoter, LastContact: api.NewReadableDuration(2 * time.Millisecond), LastTerm: 3, LastIndex: 39, Healthy: true, StableSince: time.Date(2020, 11, 6, 14, 53, 0, 0, time.UTC), }, }, } require.Equal(t, &expected, autopilotToAPIState(&input)) }