Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add antctl get fqdncache #6868

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions pkg/agent/apis/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
package apis

import (
"net"
"strconv"
"strings"
"time"

corev1 "k8s.io/api/core/v1"

Expand Down Expand Up @@ -72,6 +74,28 @@ func (r AntreaAgentInfoResponse) SortRows() bool {
return true
}

type FqdnCacheResponse struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be FQDNCacheResponse per our coding conventions
you can refer to Quan's document: https://github.com/tnqn/code-review-comments

fqdnName string
ipAddress net.IP
expirationTime time.Time
}

func (r FqdnCacheResponse) GetTableHeader() []string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you check if we can add specific test cases to cover this in code cov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to be left uncovered conventionally, as it is a simple method that primarily relies on the Transform() function to execute correctly, I don't think unit tests should be added for these funcs

return []string{"FQDN", "ADDRESS", "EXPIRATION TIME"}
}

func (r FqdnCacheResponse) GetTableRow(maxColumn int) []string {
return []string{
r.fqdnName,
r.ipAddress.String(),
r.expirationTime.String(),
}
}

func (r FqdnCacheResponse) SortRows() bool {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the default option for sorting false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the rows of the table won't need to be sorted, as there's no filter or flag for it, so the default is false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember how this function is used exactly, but it would be nice if the output rows were filtered based on the FQDN.

return false
}

type FeatureGateResponse struct {
Component string `json:"component,omitempty"`
Name string `json:"name,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions pkg/agent/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"antrea.io/antrea/pkg/agent/apiserver/handlers/bgppolicy"
"antrea.io/antrea/pkg/agent/apiserver/handlers/bgproute"
"antrea.io/antrea/pkg/agent/apiserver/handlers/featuregates"
"antrea.io/antrea/pkg/agent/apiserver/handlers/fqdncache"
"antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist"
"antrea.io/antrea/pkg/agent/apiserver/handlers/multicast"
"antrea.io/antrea/pkg/agent/apiserver/handlers/networkpolicy"
Expand Down Expand Up @@ -104,6 +105,7 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic
s.Handler.NonGoRestfulMux.HandleFunc("/bgppolicy", bgppolicy.HandleFunc(bgpq))
s.Handler.NonGoRestfulMux.HandleFunc("/bgppeers", bgppeer.HandleFunc(bgpq))
s.Handler.NonGoRestfulMux.HandleFunc("/bgproutes", bgproute.HandleFunc(bgpq))
s.Handler.NonGoRestfulMux.HandleFunc("/fqdncache", fqdncache.HandleFunc(aq))
}

func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error {
Expand Down
37 changes: 37 additions & 0 deletions pkg/agent/apiserver/handlers/fqdncache/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2025 Antrea Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fqdncache

import (
"encoding/json"
"net/http"

"k8s.io/klog/v2"

agentquerier "antrea.io/antrea/pkg/agent/querier"
)

func HandleFunc(aq agentquerier.AgentQuerier) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dnsEntryCache := aq.GetFqdnCache()
if dnsEntryCache == nil {
return
}
Comment on lines +29 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that your GetFqdnCache function will never return nil as far as I can tell. It's fine to keep the code as is, but maybe add a comment to this effect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that was for the older function, when it would return nil instead of empty, but that's fixed now, so I can remove this bit of code.

if err := json.NewEncoder(w).Encode(dnsEntryCache); err != nil {
http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError)
klog.ErrorS(err, "Failed to encode response")
}
}
}
125 changes: 125 additions & 0 deletions pkg/agent/apiserver/handlers/fqdncache/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2025 Antrea Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fqdncache

import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

queriertest "antrea.io/antrea/pkg/agent/querier/testing"
"antrea.io/antrea/pkg/agent/types"
)

func TestFqdnCacheQuery(t *testing.T) {
tests := []struct {
name string
expectedStatus int
expectedResponse []types.DnsCacheEntry
}{
{
name: "FQDN cache exists",
expectedStatus: http.StatusOK,
expectedResponse: []types.DnsCacheEntry{
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add UT cases for 1. multiple addresses for the same FQDN 2. multiple FQDN <-> address mappings

FqdnName: "example.com",
IpAddress: net.ParseIP("10.0.0.1"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
},
},
{
name: "FQDN cache exists - multiple addresses same domain",
expectedStatus: http.StatusOK,
expectedResponse: []types.DnsCacheEntry{
{
FqdnName: "example.com",
IpAddress: net.ParseIP("10.0.0.1"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "example.com",
IpAddress: net.ParseIP("10.0.0.2"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "example.com",
IpAddress: net.ParseIP("10.0.0.3"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
},
},
{
name: "FQDN cache exists - multiple addresses multiple domains",
expectedStatus: http.StatusOK,
expectedResponse: []types.DnsCacheEntry{
{
FqdnName: "example.com",
IpAddress: net.ParseIP("10.0.0.1"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "foo.com",
IpAddress: net.ParseIP("10.0.0.4"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "bar.com",
IpAddress: net.ParseIP("10.0.0.5"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
},
},
{
name: "FQDN cache does not exist",
expectedStatus: http.StatusOK,
expectedResponse: []types.DnsCacheEntry{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
q := queriertest.NewMockAgentQuerier(ctrl)
q.EXPECT().GetFqdnCache().Return(tt.expectedResponse)
handler := HandleFunc(q)
req, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
assert.Equal(t, tt.expectedStatus, recorder.Code)
if tt.expectedStatus == http.StatusOK {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just remove the if here, as the status code is always 200

Note that this unit test actually validates very little (which is expected), so in my opinion we didn't really need multiple test cases, just the second one ("FQDN cache exists - multiple addresses multiple domains") would have been fine as a basic (not table-driven) test.

var receivedResponse []map[string]interface{}
err = json.Unmarshal(recorder.Body.Bytes(), &receivedResponse)
require.NoError(t, err)
for i, rec := range receivedResponse {
parsedTime, err := time.Parse(time.RFC3339, rec["expirationTime"].(string))
require.NoError(t, err)
assert.Equal(t, tt.expectedResponse[i], types.DnsCacheEntry{
FqdnName: rec["fqdnName"].(string),
IpAddress: net.ParseIP(rec["ipAddress"].(string)),
ExpirationTime: parsedTime,
})
}
}
})
}
}
11 changes: 11 additions & 0 deletions pkg/agent/controller/networkpolicy/networkpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,17 @@ func NewNetworkPolicyController(antreaClientGetter client.AntreaClientProvider,
return c, nil
}

func (c *Controller) GetFqdnCache() []types.DnsCacheEntry {
cacheEntryList := []types.DnsCacheEntry{}
for _, dnsMeta := range c.fqdnController.dnsEntryCache {
for fqdn, ipWithExpiration := range dnsMeta.responseIPs {
entry := types.DnsCacheEntry{FqdnName: fqdn, IpAddress: ipWithExpiration.ip, ExpirationTime: ipWithExpiration.expirationTime}
cacheEntryList = append(cacheEntryList, entry)
}
}
return cacheEntryList
}

func (c *Controller) GetNetworkPolicyNum() int {
return c.ruleCache.GetNetworkPolicyNum()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -903,3 +903,61 @@ func TestValidate(t *testing.T) {
t.Fatalf("groupAddress %s expect %v, but got %v", groupAddress2, v1beta1.RuleActionDrop, item.RuleAction)
}
}

func TestGetFqdnCache(t *testing.T) {
controller, _, _ := newTestController()
expectedEntryList := []agenttypes.DnsCacheEntry{}
assert.Equal(t, expectedEntryList, controller.GetFqdnCache())

controller.fqdnController.dnsEntryCache = map[string]dnsMeta{
"*.example.com": {
responseIPs: map[string]ipWithExpiration{
"maps.example.com": {
ip: net.ParseIP("10.0.0.1"),
expirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
"mail.example.com": {
ip: net.ParseIP("10.0.0.2"),
expirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
"photos.example.com": {
ip: net.ParseIP("10.0.0.3"),
expirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
},
},
"antrea.io": {
responseIPs: map[string]ipWithExpiration{
"antrea.io": {
ip: net.ParseIP("10.0.0.4"),
expirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
},
},
Comment on lines +913 to +936
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test input data doesn't match what we actually store in the cache
There is never a wildcard domain ("*.example.com") as the key in the dnsEntryCache map
For the nested map (map[string]ipWithExpiration) the key is actually the string representation of the IP (for historical reasons). See:

// Key for responseIPs is the string representation of the IP.
// It helps to quickly identify IP address updates when a
// new DNS response is received.
responseIPs map[string]ipWithExpiration

So it's actually like this:

		"antrea.io": {
			responseIPs: map[string]ipWithExpiration{
				""10.0.0.4"": {
					ip:             net.ParseIP("10.0.0.4"),
					expirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
				},
			},
		},

}

expectedEntryList = []agenttypes.DnsCacheEntry{
{
FqdnName: "maps.example.com",
IpAddress: net.ParseIP("10.0.0.1"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "mail.example.com",
IpAddress: net.ParseIP("10.0.0.2"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "photos.example.com",
IpAddress: net.ParseIP("10.0.0.3"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
{
FqdnName: "antrea.io",
IpAddress: net.ParseIP("10.0.0.4"),
ExpirationTime: time.Date(2025, 12, 25, 15, 0, 0, 0, time.UTC),
},
}
returnedList := controller.GetFqdnCache()
assert.ElementsMatch(t, expectedEntryList, returnedList)
}
7 changes: 7 additions & 0 deletions pkg/agent/querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"antrea.io/antrea/pkg/agent/memberlist"
"antrea.io/antrea/pkg/agent/openflow"
"antrea.io/antrea/pkg/agent/proxy"
"antrea.io/antrea/pkg/agent/types"
"antrea.io/antrea/pkg/apis/crd/v1beta1"
"antrea.io/antrea/pkg/ovs/ovsconfig"
"antrea.io/antrea/pkg/ovs/ovsctl"
Expand All @@ -47,6 +48,7 @@ type AgentQuerier interface {
GetMemberlistCluster() memberlist.Interface
GetNodeLister() corelisters.NodeLister
GetBGPPolicyInfoQuerier() querier.AgentBGPPolicyInfoQuerier
GetFqdnCache() []types.DnsCacheEntry
}

type agentQuerier struct {
Expand Down Expand Up @@ -97,6 +99,11 @@ func NewAgentQuerier(
}
}

// GetFQDNCache returns dnsEntryCache within fqdnController
func (aq agentQuerier) GetFqdnCache() []types.DnsCacheEntry {
return aq.networkPolicyInfoQuerier.GetFqdnCache()
}

// GetNodeLister returns NodeLister.
func (aq agentQuerier) GetNodeLister() corelisters.NodeLister {
return aq.nodeLister
Expand Down
15 changes: 15 additions & 0 deletions pkg/agent/querier/testing/mock_querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkg/agent/types/networkpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
package types

import (
"net"
"time"

"k8s.io/apimachinery/pkg/util/sets"

"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
secv1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1"
binding "antrea.io/antrea/pkg/ovs/openflow"
)

type DnsCacheEntry struct {
FqdnName string `json:"fqdnName,omitempty"`
IpAddress net.IP `json:"ipAddress,omitempty"`
ExpirationTime time.Time `json:"expirationTime,omitempty"`
}

type MatchKey struct {
ofProtocol binding.Protocol
valueCategory AddressCategory
Expand Down
Loading
Loading