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 tests for distroless image #7

Open
wants to merge 2 commits into
base: master
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
18 changes: 12 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,25 @@ fmt: ## Check formatting
exit 1; \
fi

check: check-debian check-debian-nosanity check-debian-backports check-fedora check-alpine
build-tests: $(BIN_DIR)
$(GO) test ./test -c -o $(BIN_DIR)/tests

check-debian: build
check: check-debian check-debian-nosanity check-debian-backports check-fedora check-alpine check-distroless

check-debian: build build-tests
./test/run-test.sh --build-fail debian

check-debian-nosanity: build
check-debian-nosanity: build build-tests
./test/run-test.sh --build-arg="INSTALL_ARGS=--no-sanity-check" --nft-fail debian-nosanity

check-debian-backports: build
check-debian-backports: build build-tests
./test/run-test.sh --build-arg="REPO=buster-backports" debian-backports

check-fedora: build
check-fedora: build build-tests
./test/run-test.sh fedora

check-alpine: build
check-alpine: build build-tests
./test/run-test.sh alpine

check-distroless: build build-tests
./test/run-test.sh distroless
26 changes: 17 additions & 9 deletions internal/iptables/alternatives.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func BuildAlternativeSelector(sbinPath string) AlternativeSelector {
return updateAlternativesSelector{sbinPath: sbinPath}
} else {
// if we don't find any tool to managed the alternatives, handle it manually with symlinks
return symlinkSelector{sbinPath: sbinPath}
return NewSymlinker(sbinPath)
}
}

Expand Down Expand Up @@ -79,26 +79,34 @@ func (a alternativesSelector) UseMode(ctx context.Context, mode Mode) error {
return nil
}

// symlinkSelector manages an iptables setup by manually creating symlinks
// that point to the proper "mode" binaries.
func NewSymlinker(sbinPath string) Symlinker {
return Symlinker{sbinPath: sbinPath}
}

// Symlinker manages an iptables setup by manually creating symlinks
// for all iptables commands.
// It configures: `iptables`, `iptables-save`, `iptables-restore`,
// `ip6tables`, `ip6tables-save` and `ip6tables-restore`.
type symlinkSelector struct {
type Symlinker struct {
sbinPath string
}

func (s symlinkSelector) UseMode(ctx context.Context, mode Mode) error {
modeStr := string(mode)
xtablesForModePath := XtablesPath(s.sbinPath, mode)
// UseMode configures the system to use the selected iptables mode.
func (s Symlinker) UseMode(ctx context.Context, mode Mode) error {
return s.LinkAll(ctx, XtablesPath(s.sbinPath, mode))
}

// LinkAll creates symlinks for all iptables commands to the targetPath.
func (s Symlinker) LinkAll(ctx context.Context, targetPath string) error {
cmds := []string{"iptables", "iptables-save", "iptables-restore", "ip6tables", "ip6tables-save", "ip6tables-restore"}

for _, cmd := range cmds {
cmdPath := filepath.Join(s.sbinPath, cmd)
// If deleting fails, ignore it and try to create symlink regardless
_ = os.RemoveAll(cmdPath)

if err := os.Symlink(xtablesForModePath, cmdPath); err != nil {
return fmt.Errorf("creating %s symlink for mode %s: %v", cmd, modeStr, err)
if err := os.Symlink(targetPath, cmdPath); err != nil {
return fmt.Errorf("creating %s symlink to %s: %v", cmd, targetPath, err)
}
}

Expand Down
14 changes: 7 additions & 7 deletions internal/iptables/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func DetectBinaryDir() (string, error) {
type Mode string

const (
legacy Mode = "legacy"
nft Mode = "nft"
Legacy Mode = "legacy"
NFT Mode = "nft"
)

// DetectMode inspects the current iptables entries and tries to
Expand All @@ -59,12 +59,12 @@ func DetectMode(ctx context.Context, iptables Installation) Mode {
rulesOutput := &bytes.Buffer{}
_ = iptables.NFTSave(ctx, rulesOutput, "-t", "mangle")
if hasKubeletChains(rulesOutput.Bytes()) {
return nft
return NFT
}
rulesOutput.Reset()
_ = iptables.NFTSaveIP6(ctx, rulesOutput, "-t", "mangle")
if hasKubeletChains(rulesOutput.Bytes()) {
return nft
return NFT
}
rulesOutput.Reset()

Expand All @@ -74,14 +74,14 @@ func DetectMode(ctx context.Context, iptables Installation) Mode {
// exist, which we don't want. So we have to grab all the rules.
_ = iptables.LegacySave(ctx, rulesOutput)
if hasKubeletChains(rulesOutput.Bytes()) {
return legacy
return Legacy
}
rulesOutput.Reset()
_ = iptables.LegacySaveIP6(ctx, rulesOutput)
if hasKubeletChains(rulesOutput.Bytes()) {
return legacy
return Legacy
}

// If we can't detect any of the 2 patterns, default to nft.
return nft
return NFT
}
6 changes: 3 additions & 3 deletions iptables-wrapper-installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ case "${altstyle}" in
--install /usr/sbin/iptables iptables /usr/sbin/iptables-wrapper 100 \
--slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-wrapper \
--slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-wrapper \
--slave /usr/sbin/ip6tables iptables /usr/sbin/iptables-wrapper \
--slave /usr/sbin/ip6tables-restore iptables-restore /usr/sbin/iptables-wrapper \
--slave /usr/sbin/ip6tables-save iptables-save /usr/sbin/iptables-wrapper
--slave /usr/sbin/ip6tables ip6tables /usr/sbin/iptables-wrapper \
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 could be wrong here bu I think before this change it wasn't creating symlinks for ip6tables-* because the name overlapped with the previous slaves. Or at least that's what I found during testing.

--slave /usr/sbin/ip6tables-restore ip6tables-restore /usr/sbin/iptables-wrapper \
--slave /usr/sbin/ip6tables-save ip6tables-save /usr/sbin/iptables-wrapper
;;

debian)
Expand Down
46 changes: 44 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ it will enter an infinite loop, calling itself recursively.

It's important to note that this proxy behavior will only happen on the first iptables-*
execution. Following invocations will use directly the binaries for the selected mode.

If the command is executed with the `install` argument, it will create symlinks for all iptables binaries
pointing to itself. The second argument must be the path where the symlinks should be installed, preferably
the same path where iptables is installed.
This is useful for the installation process of the iptables-wrapper itself before first execution.
*/
package main

Expand All @@ -41,17 +46,49 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/kubernetes-sigs/iptables-wrappers/internal/iptables"
)

func main() {
ctx := context.Background()

if installMode() {
install(ctx)
return
}

forward(ctx)
}

func installMode() bool {
return len(os.Args) == 3 && os.Args[1] == "install"
}

func installFolder() string {
return os.Args[2]
}

// install creates symlinks for all iptables binaries in the given folder
// pointing to the current binary being executed.
func install(ctx context.Context) {
wrapperPath, err := os.Executable()
if err != nil {
fatal(err)
}
wrapperPath = filepath.Clean(wrapperPath)

if err := iptables.NewSymlinker(installFolder()).LinkAll(ctx, wrapperPath); err != nil {
fatal(err)
}
}

// forward detects the iptables mode to use and re-executes the exact same command passed to this program.
func forward(ctx context.Context) {
sbinPath, err := iptables.DetectBinaryDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
fatal(err)
}

// We use `xtables-<mode>-multi` binaries by default to inspect the installed rules,
Expand Down Expand Up @@ -91,3 +128,8 @@ func main() {
os.Exit(code)
}
}

func fatal(err error) {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
4 changes: 2 additions & 2 deletions test/Dockerfile.test-alpine
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

FROM alpine:3.15

RUN apk add --no-cache iptables
RUN apk add --no-cache iptables ip6tables
COPY iptables-wrapper-installer.sh /
COPY bin/iptables-wrapper /
RUN /iptables-wrapper-installer.sh
COPY test/test.sh /
COPY bin/tests /
4 changes: 2 additions & 2 deletions test/Dockerfile.test-debian
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ FROM debian:buster
ARG INSTALL_ARGS=
ARG REPO=buster

RUN echo deb http://deb.debian.org/debian buster-backports main >> /etc/apt/sources.list; \
RUN echo deb http://archive.debian.org/debian buster-backports main >> /etc/apt/sources.list; \
Copy link
Contributor Author

Choose a reason for hiding this comment

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

they changed this url for buster and that broke the test

apt-get update; \
apt-get -t ${REPO} -y --no-install-recommends install iptables

COPY iptables-wrapper-installer.sh /
COPY bin/iptables-wrapper /
RUN /iptables-wrapper-installer.sh ${INSTALL_ARGS}
COPY test/test.sh /
COPY bin/tests /
36 changes: 36 additions & 0 deletions test/Dockerfile.test-distroless
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2023 The Kubernetes 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.

### Dockerfile for building a distroless image for testing
# Modified version of https://github.com/kubernetes/release/blob/master/images/build/distroless-iptables/distroless/Dockerfile

ARG STAGE_DIR="/opt/stage"

FROM debian:bullseye-slim AS build
ARG STAGE_DIR

COPY test/build/stage-binaries-from-package.sh /
COPY test/build/package-utils.sh /
COPY test/build/stage-binary-and-deps.sh /

RUN mkdir -p "${STAGE_DIR}" && \
/stage-binaries-from-package.sh "${STAGE_DIR}" iptables

FROM gcr.io/distroless/static
ARG STAGE_DIR

COPY --from=build "${STAGE_DIR}" /
COPY bin/iptables-wrapper /usr/sbin/iptables-wrapper
RUN ["/usr/sbin/iptables-wrapper", "install", "/usr/sbin"]
COPY bin/tests /
2 changes: 1 addition & 1 deletion test/Dockerfile.test-fedora
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ RUN dnf install -y iptables iptables-legacy iptables-nft
COPY iptables-wrapper-installer.sh /
COPY bin/iptables-wrapper /
RUN /iptables-wrapper-installer.sh
COPY test/test.sh /
COPY bin/tests /
48 changes: 48 additions & 0 deletions test/build/package-utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# Copied from https://github.com/kubernetes/release/blob/master/images/build/distroless-iptables/distroless/package-utils.sh

# Copyright 2022 The Kubernetes 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.

# file_to_package identifies the debian package that provided the file $1
file_to_package() {
# `dpkg-query --search $file-pattern` outputs lines with the format: "$package: $file-path"
# where $file-path belongs to $package
# https://manpages.debian.org/jessie/dpkg/dpkg-query.1.en.html
dpkg-query --search "$(realpath "${1}")" | cut -d':' -f1
}

# package_to_copyright gives the path to the copyright file for the package $1
package_to_copyright() {
echo "/usr/share/doc/${1}/copyright"
}

# stage_file stages the filepath $1 to $2, following symlinks
# and staging copyrights
stage_file() {
cp -a --parents "${1}" "${2}"
# recursively follow symlinks
if [[ -L "${1}" ]]; then
stage_file "$(cd "$(dirname "${1}")" || exit; realpath -s "$(readlink "${1}")")" "${2}"
fi
# get the package so we can stage package metadata as well
package="$(file_to_package "${1}")"
# stage the copyright for the file
cp -a --parents "$(package_to_copyright "${package}")" "${2}"
# stage the package status mimicking bazel
# https://github.com/bazelbuild/rules_docker/commit/f5432b813e0a11491cf2bf83ff1a923706b36420
# instead of parsing the control file, we can just get the actual package status with dpkg
dpkg -s "${package}" > "${2}/var/lib/dpkg/status.d/${package}"
}
65 changes: 65 additions & 0 deletions test/build/stage-binaries-from-package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash

# Copied from https://github.com/kubernetes/release/blob/master/images/build/distroless-iptables/distroless/stage-binaries-from-package.sh

# Copyright 2022 The Kubernetes 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.

# USAGE: stage-binaries-from-package.sh /opt/stage package1 package2
#
# Stages all the packages and its dependencies (+ libraries and copyrights) to $1
#
# This is intended to be used in a multi-stage docker build with a distroless/base
# or distroless/cc image.
set -e

. package-utils.sh

stage_file_list() {
IFS="
"
REQUIRED_FILES="$(dpkg -L "${1}" | grep -vE '(/\.|/s?bin/|/usr/share/(man|doc|.*-completion))' | sed 's/\n/ /g')"
for file in $REQUIRED_FILES; do
if [ -f "$file" ]; then
stage_file "${file}" "${STAGE_DIR}"
fi
done

BIN_LIST="$(dpkg -L "${1}" | grep -E '/s?bin/' |sed 's/\n/ /g')"
for binary in $BIN_LIST; do
/stage-binary-and-deps.sh "${2}" "${binary}"
done
}

get_dependent_packages() {
apt-cache depends "${1}" |grep Depends|awk -F '.*Depends:[[:space:]]?' '{print $2}'
}

main() {
STAGE_DIR="${1}/"
mkdir -p "${STAGE_DIR}"/var/lib/dpkg/status.d/
apt-get -y update
shift
while (( "$#" )); do # While there are arguments still to be shifted
PACKAGE="${1}"
apt-get -y install "${PACKAGE}"
stage_file_list "${PACKAGE}" "$STAGE_DIR"
while IFS= read -r c_dep; do
stage_file_list "${c_dep}" "${STAGE_DIR}"
done < <(get_dependent_packages "${PACKAGE}")
shift
done
}

main "$@"
Loading