// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package agent import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/grpc-external/limiter" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/testrpc" ) func TestAgent_local_proxycfg(t *testing.T) { a := NewTestAgent(t, TestACLConfig()) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") token := generateUUID() svc := &structs.NodeService{ ID: "db", Service: "db", Port: 5000, EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), } require.NoError(t, a.State.AddServiceWithChecks(svc, nil, token, true)) proxy := &structs.NodeService{ Kind: structs.ServiceKindConnectProxy, ID: "db-sidecar-proxy", Service: "db-sidecar-proxy", Port: 5000, // Set this internal state that we expect sidecar registrations to have. LocallyRegisteredAsSidecar: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "db", Upstreams: structs.TestUpstreams(t, false), }, EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), } require.NoError(t, a.State.AddServiceWithChecks(proxy, nil, token, true)) // This is a little gross, but this gives us the layered pair of // local/catalog sources for now. cfg := a.xdsServer.ProxyWatcher var ( timer = time.After(100 * time.Millisecond) timerFired = false finalTimer <-chan time.Time ) var ( firstTime = true ch <-chan *proxycfg.ConfigSnapshot stc limiter.SessionTerminatedChan cancel context.CancelFunc ) defer func() { if cancel != nil { cancel() } }() for { if ch == nil { // Sign up for a stream of config snapshots, in the same manner as the xds server. sid := proxy.CompoundServiceID() if firstTime { firstTime = false } else { t.Logf("re-creating watch") } // Prior to fixes in https://github.com/hashicorp/consul/pull/16497 // this call to Watch() would deadlock. var err error ch, stc, _, cancel, err = cfg.Watch(sid, a.config.NodeName, token) require.NoError(t, err) } select { case <-stc: t.Fatal("session unexpectedly terminated") case snap, ok := <-ch: if !ok { t.Logf("channel is closed") cancel() ch, stc, cancel = nil, nil, nil continue } require.NotNil(t, snap) if !timerFired { t.Fatal("should not have gotten snapshot until after we manifested the token") } return case <-timer: timerFired = true finalTimer = time.After(1 * time.Second) // This simulates the eventual consistency of a token // showing up on a server after it's creation by // pre-creating the UUID and later using that as the // initial SecretID for a real token. gotToken := testWriteToken(t, a, &api.ACLToken{ AccessorID: generateUUID(), SecretID: token, Description: "my token", ServiceIdentities: []*api.ACLServiceIdentity{{ ServiceName: "db", }}, }) require.Equal(t, token, gotToken) case <-finalTimer: t.Fatal("did not receive a snapshot after the token manifested") } } } func testWriteToken(t *testing.T, a *TestAgent, tok *api.ACLToken) string { req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonReader(tok)) req.Header.Add("X-Consul-Token", "root") resp := httptest.NewRecorder() a.srv.h.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) dec := json.NewDecoder(resp.Body) aclResp := &structs.ACLToken{} require.NoError(t, dec.Decode(aclResp)) return aclResp.SecretID }