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

Feature/podmannet #37

Merged
merged 6 commits into from
Jan 16, 2024
Merged
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
1 change: 1 addition & 0 deletions decorator/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ import (
_ "github.com/siemens/ghostwire/v2/decorator/dockerproxy" // activate nerdctl-managed CNI network alias name decoration.
_ "github.com/siemens/ghostwire/v2/decorator/ieappicon" // include (on-demand) IE App icon decoration.
_ "github.com/siemens/ghostwire/v2/decorator/nerdctlnet" // activate nerdctl-managed CNI network alias name decoration.
_ "github.com/siemens/ghostwire/v2/decorator/podmannet" // activate podman-managed network alias name decoration.
)
40 changes: 21 additions & 19 deletions decorator/dockernet/dockernet.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,37 @@ type dockerNetworks struct {
engineNetns *network.NetworkNamespace // ...of the managing Docker engine.
}

// makeDockerNetworks returns a dockerNetworks with the networks managed by the
// specified Docker engine. If discovery failed, a zero dockerNetworks will be
// returned instead, to be used in the engine map to signal that the asked the
// engine, but it failed, so no more attempts to talk to it, please.
// makeDockerNetworks returns a dockerNetworks object with the networks managed
// by the specified Docker engine. If discovery failed, a zero-value
// dockerNetworks object will be returned instead, to be used in the engine map
// to signal that we asked the engine, but it failed, so no more attempts to
// talk to it, please.
func makeDockerNetworks(ctx context.Context, engine *model.ContainerEngine, allnetns network.NetworkNamespaces) (
docknets dockerNetworks,
) {
dockerclient, err := client.NewClientWithOpts(
client.WithHost(engine.API),
client.WithAPIVersionNegotiation())
if err == nil {
networks, _ := dockerclient.NetworkList(ctx, types.NetworkListOptions{})
_ = dockerclient.Close()
netnsid, _ := ops.NamespacePath(fmt.Sprintf("/proc/%d/ns/net", engine.PID)).ID()
docknets.networks = networks
docknets.engine = engine
docknets.engineNetns = allnetns[netnsid]
log.Infof("found %d Docker networks related to net:[%d] %s",
len(networks), docknets.engineNetns.ID().Ino, docknets.engineNetns.DisplayName())
} else {
log.Warnf("cannot discover Docker-managed networks, API %s", engine.API)
if err != nil {
log.Warnf("cannot discover Docker-managed networks from API %s, reason: %s",
engine.API, err.Error())
return
}
networks, _ := dockerclient.NetworkList(ctx, types.NetworkListOptions{})
_ = dockerclient.Close()
netnsid, _ := ops.NamespacePath(fmt.Sprintf("/proc/%d/ns/net", engine.PID)).ID()
docknets.networks = networks
docknets.engine = engine
docknets.engineNetns = allnetns[netnsid]
log.Infof("found %d Docker networks related to net:[%d] %s",
len(networks), docknets.engineNetns.ID().Ino, docknets.engineNetns.DisplayName())
return
}

// Decorate decorates bridge network interfaces with alias names that are the
// names of their corresponding Docker "bridge" networks, where applicable (a
// copy is stored also in the labels in Gostwire's key namespace). Additionally,
// it copies over any user-defined network labels.
// Decorate decorates bridge and macvlan master network interfaces with alias
// names that are the names of their corresponding Docker bridge” or “macvlan”
// networks, where applicable (a copy is stored also in the labels in Gostwire's
// key namespace). Additionally, it copies over any user-defined network labels.
func Decorate(
ctx context.Context,
allnetns network.NetworkNamespaces,
Expand Down
11 changes: 11 additions & 0 deletions decorator/podmannet/_test/pind/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Based on https://www.redhat.com/sysadmin/podman-inside-container
ARG FEDORA_TAG

FROM fedora:${FEDORA_TAG}
RUN dnf -y install \
procps systemd podman fuse-overlayfs \
--exclude container-selinux && \
dnf clean all && \
rm -rf /var/cache /var/log/dnf* /var/log/yum.* && \
systemctl enable podman.socket
CMD [ "/usr/sbin/init" ]
29 changes: 29 additions & 0 deletions decorator/podmannet/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Package podmannet implements a Gostwire decorator that discovers podman (v4+)
managed networks and then decorates their corresponding Linux-kernel network
interfaces. Supported types of podman networks are “bridge” and “macvlan”.

In case of “bridge” networks this decorator assigns network names as alias names
to the corresponding Linux-kernel bridges and also as a Gostwire-specific label.

For “MACVLAN” networks this decorator assigns the network names as alias names
to the “parent” network interface (or “master” in Linux parlance).

This decorator also copies any network labels it finds into the corresponding
network.Interface instances in a Gostwire discovery information model.

# Note

The Docker-compatible podman API is subtly incompatible: it uses a different
bridge name-allocating method, and it doesn't reveal the bridge and macvlan
master names.

In consequence, we need to resort to a self-rolled minimal HTTP-over-UDS client
that supports a minimal subset of the podman-proprietary libpod API. As of
podman v4 the libpod API endpoint returns network information. As a nice
benefit, the network information endpoint abstracts from the different podmen
networking mechanisms, that is, CNI-based and/or [netavark]-based.

[netavark]: https://github.com/containers/netavark
*/
package podmannet
167 changes: 167 additions & 0 deletions decorator/podmannet/libpodclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// (c) Siemens AG 2024
//
// SPDX-License-Identifier: MIT

package podmannet

import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path"
"time"
)

// UserAgent specifies the HTTP agent string used when talking to podman's
// libpod API.
const UserAgent = "Gostwire (The Sequel)"

// Client is a minimalist HTTP-over-UDS (unix domain socket) client for
// conversing with podmen libpod API endpoints.
type Client struct {
httpClient *http.Client
endpointURL *url.URL
libpodVersion string // libpod API semver, without "v" prefix.
}

// newLibpodClient returns a new podman libpod API client. The endpoint must be
// using the "unix" protocol.
//
// Please note that this libpod API client is absolutely minimalist and just
// suffices for querying the podman-managed networks.
func newLibpodClient(endpoint string) (*Client, error) {
epurl, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("invalid endpoint, reason: %w", err)
}
if epurl.Scheme != "unix" {
return nil, fmt.Errorf("unsupported endpoint protocol '%s'", epurl.Scheme)
}
c := &Client{
httpClient: &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
},
endpointURL: epurl,
}
dialer := &net.Dialer{
// same as Docker's unix socket default transport configuration, see
// also
// https://github.com/docker/go-connections/blob/fa09c952e3eadbffaf8afc5b8a1667158ba38ace/sockets/sockets.go#L11
Timeout: 10 * time.Second,
}
c.httpClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, _ string, _ string) (net.Conn, error) {
// we don't want to dial the libpod API endpoint, but instead the engine
// API endpoint as such...
return dialer.DialContext(ctx, epurl.Scheme, epurl.Path)
}
return c, nil
}

// Close closes idle connections.
func (c *Client) Close() error {
if c.httpClient == nil {
return nil
}
c.httpClient.CloseIdleConnections()
return nil
}

// apiPath takes a non-versioned libpod API endpoint, such as “/info” and
// “networks/json”; it then returns a versioned libpod path when the libpod
// version is already known, such as “/v1.2.3/libpod/networks/json”. Otherwise
// it returns a “/v0/libpod/...”-based path. In consequence, without the
// libpodVersion set on the Client, only use the “/info” service endpoint, as
// this seems to be version-independent, but still needs any version in its
// endpoint path.
func (c *Client) apiPath(apipath string) string {
if c.libpodVersion == "" {
// use only for initial libpod info (API version) retrieval; please note
// that all libpod API endpoints are versioned, there are not
// un-versioned endpoints like the Docker API does.
return path.Join("/v0/libpod", apipath)
}
return path.Join("/v"+c.libpodVersion+"/libpod", apipath)
}

// get issues an HTTP GET request for the specified (yet unversioned) API
// endpoint, such as “/networks/json”. It then returns the HTTP response or an
// error.
func (c *Client) get(ctx context.Context, apipath string) (*http.Response, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"http://localhost"+c.apiPath(apipath),
nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", UserAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("podman service returned status code %d", resp.StatusCode)
}
return resp, nil
}

// ensureReaderClosed helper to drain any service response.
func ensureReaderClosed(resp *http.Response) {
if resp.Body == nil {
return
}
_, _ = io.CopyN(io.Discard, resp.Body, 512)
resp.Body.Close()
}

// essentialLibpodInformation grabs just the API version information from the
// JSON salad returned by a “/vX/libpod/info” endpoint.
type essentialLibpodInformation struct {
Version struct {
APIVersion string // major.minor.patch, without "v" prefix
} `json:"version"`
}

// info returns the “essential” libpod information, that is, the libpod API
// version.
func (c *Client) info(ctx context.Context) (essentialLibpodInformation, error) {
resp, err := c.get(ctx, "/info")
var info essentialLibpodInformation
if err != nil {
return info, err
}
err = json.NewDecoder(resp.Body).Decode(&info)
return info, err
}

// NetworkResource grabs just the few things from a podman network we're
// interested here for the purposes of correctly decorating network interfaces
// with podman network names. We simply ignore all the other JSON salad returned
// from the “/vX/libpod/networks/json” endpoint.
type NetworkResource struct {
Name string `json:"name"` // name of the network
ID string `json:"id"` // unique ID within the particular podman engine instance
Driver string `json:"driver"` // name of the driver; "bridge", "macvlan", "ipvlan"
NetworkInterface string `json:"network_interface"` // name of the associated (master) network interface
Internal bool `json:"internal"` // network is host-internal only, without external connectivity
Labels map[string]string `json:"labels"`
}

// networkList returns the list of managed podman networks.
func (c *Client) networkList(ctx context.Context) ([]NetworkResource, error) {
var netrscs []NetworkResource
resp, err := c.get(ctx, "/networks/json")
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
}
err = json.NewDecoder(resp.Body).Decode(&netrscs)
return netrscs, err
}
17 changes: 17 additions & 0 deletions decorator/podmannet/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// (c) Siemens AG 2023
//
// SPDX-License-Identifier: MIT

package podmannet

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestGostwireDecoratorPodmannet(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "ghostwire/decorator/podmannet package")
}
Loading
Loading