diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index df33b35..f68ba0a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,11 +27,27 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Setup tools - run: make setup + run: make setup download-cilium-cli - name: Run code check run: make check-generate - name: Run lint run: make lint + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build cilium-agent-proxy + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + load: true + push: false + tags: cilium-agent-proxy:dev - name: Run environment working-directory: e2e run: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 33a0a69..f16a258 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,6 +24,23 @@ jobs: echo "Tag v${{ inputs.tag }} already exists" exit 1 fi + - name: Download Cilium CLI + run: make download-cilium-cli + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push cilium-agent-proxy + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: ghcr.io/cybozu-go/cilium-agent-proxy:${{ inputs.tag }} - uses: actions/setup-go@v4 with: go-version-file: go.mod diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..98b9d17 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Build the manager binary +FROM ghcr.io/cybozu/golang:1.23-jammy AS builder + +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/cilium-agent-proxy/ cmd/cilium-agent-proxy/ +COPY Makefile Makefile + +# Build +RUN make build-proxy + +# Compose the manager container +FROM ghcr.io/cybozu/ubuntu:22.04 +LABEL org.opencontainers.image.source=https://github.com/cybozu-go/network-policy-viewer + +WORKDIR / +COPY bin/download/cilium / +COPY --from=builder /work/bin/cilium-agent-proxy / + +ENTRYPOINT ["/cilium-agent-proxy"] diff --git a/Makefile b/Makefile index ba4dfdb..bf94765 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ TOOLS_DIR := $(BIN_DIR)/download CACHE_DIR := $(shell pwd)/cache # Test tools +CILIUM_IMAGE_VERSION := 1.14.14.1 +CILIUM_CLI := $(TOOLS_DIR)/cilium CUSTOMCHECKER := $(TOOLS_DIR)/custom-checker HELM := helm --repository-cache $(CACHE_DIR)/helm/repository --repository-config $(CACHE_DIR)/helm/repositories.yaml STATICCHECK := $(TOOLS_DIR)/staticcheck @@ -26,6 +28,13 @@ setup: $(CUSTOMCHECKER) $(STATICCHECK) ## Install necessary tools $(HELM) repo add cilium https://helm.cilium.io/ $(HELM) repo update cilium +.PHONY: download-cilium-cli +download-cilium-cli: + mkdir -p $(TOOLS_DIR) + CONTAINER_ID=$$(docker run --rm --detach --entrypoint pause ghcr.io/cybozu/cilium:$(CILIUM_IMAGE_VERSION)); \ + docker cp $${CONTAINER_ID}:/usr/bin/cilium $(CILIUM_CLI); \ + docker stop $${CONTAINER_ID} + $(CUSTOMCHECKER): GOBIN=$(TOOLS_DIR) go install github.com/cybozu-go/golang-custom-analyzer/cmd/custom-checker@latest @@ -42,7 +51,12 @@ clean: .PHONY: build build: ## Build network-policy-viewer mkdir -p $(BIN_DIR) - go build -trimpath -ldflags "-w -s" -o $(BIN_DIR)/npv main.go + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-w -s" -o $(BIN_DIR)/npv ./cmd/npv + +.PHONY: build-proxy +build-proxy: ## Build cilium-agent-proxy + mkdir -p $(BIN_DIR) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-w -s" -o $(BIN_DIR)/cilium-agent-proxy ./cmd/cilium-agent-proxy .PHONY: check-generate check-generate: diff --git a/cmd/cilium-agent-proxy/app/helper.go b/cmd/cilium-agent-proxy/app/helper.go new file mode 100644 index 0000000..00567f8 --- /dev/null +++ b/cmd/cilium-agent-proxy/app/helper.go @@ -0,0 +1,53 @@ +package app + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os/exec" + "path/filepath" + "strconv" +) + +const ( + ciliumPath = "/cilium" +) + +func runCommand(path string, input []byte, args ...string) ([]byte, []byte, error) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + cmd := exec.Command(path, args...) + cmd.Stdout = stdout + cmd.Stderr = stderr + if input != nil { + cmd.Stdin = bytes.NewReader(input) + } + if err := cmd.Run(); err != nil { + _, file := filepath.Split(path) + return stdout.Bytes(), stderr.Bytes(), fmt.Errorf("%s failed with %s: stderr=%s", file, err, stderr) + } + return stdout.Bytes(), stderr.Bytes(), nil +} + +func renderJSON(w http.ResponseWriter, path string, data []byte, status int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if _, err := w.Write(data); err != nil { + slog.Error("failed to write response", slog.String("path", path)) + } +} + +func renderError(w http.ResponseWriter, path string, message string, status int) { + slog.Info(message, slog.String("path", path), slog.Int("status", status)) + ret := make(map[string]string) + ret["error"] = message + ret["status"] = strconv.Itoa(status) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(ret); err != nil { + slog.Error("failed to write response", slog.String("path", path)) + } +} diff --git a/cmd/cilium-agent-proxy/app/root.go b/cmd/cilium-agent-proxy/app/root.go new file mode 100644 index 0000000..973860f --- /dev/null +++ b/cmd/cilium-agent-proxy/app/root.go @@ -0,0 +1,26 @@ +package app + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "cilium-agent-proxy", + Short: "cilium-agent proxy", + Long: `cilium-agent proxy`, + + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + return subMain() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/cilium-agent-proxy/app/run.go b/cmd/cilium-agent-proxy/app/run.go new file mode 100644 index 0000000..8a825c9 --- /dev/null +++ b/cmd/cilium-agent-proxy/app/run.go @@ -0,0 +1,113 @@ +package app + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "net/http" + "strconv" +) + +const socketPath = "/var/run/cilium/cilium.sock" + +var ( + socketClient *http.Client +) + +func handleEndpoint(w http.ResponseWriter, r *http.Request) { + param := r.URL.Path[len("/v1/endpoint/"):] + if len(param) == 0 { + renderError(w, r.URL.Path, "failed to read endpoint ID", http.StatusBadRequest) + return + } + + // Convert to number to avoid parameter injection + endpoint, err := strconv.Atoi(param) + if err != nil { + renderError(w, r.URL.Path, "failed to read endpoint ID", http.StatusBadRequest) + return + } + + url := fmt.Sprintf("http://localhost/v1/endpoint/%d", endpoint) + resp, err := socketClient.Get(url) + if err != nil { + renderError(w, r.URL.Path, "failed to call Cilium API", http.StatusInternalServerError) + return + } + + buf := new(bytes.Buffer) + io.Copy(buf, resp.Body) + renderJSON(w, r.URL.Path, buf.Bytes(), http.StatusOK) +} + +func handleIdentity(w http.ResponseWriter, r *http.Request) { + param := r.URL.Path[len("/v1/identity/"):] + if len(param) == 0 { + renderError(w, r.URL.Path, "failed to read identity", http.StatusBadRequest) + return + } + + // Convert to number to avoid parameter injection + identity, err := strconv.Atoi(param) + if err != nil { + renderError(w, r.URL.Path, "failed to read identity", http.StatusBadRequest) + return + } + + url := fmt.Sprintf("http://localhost/v1/identity/%d", identity) + resp, err := socketClient.Get(url) + if err != nil { + renderError(w, r.URL.Path, "failed to call Cilium API", http.StatusInternalServerError) + return + } + + buf := new(bytes.Buffer) + io.Copy(buf, resp.Body) + renderJSON(w, r.URL.Path, buf.Bytes(), http.StatusOK) +} + +func handlePolicy(w http.ResponseWriter, r *http.Request) { + param := r.URL.Path[len("/policy/"):] + if len(param) == 0 { + renderError(w, r.URL.Path, "failed to read endpoint ID", http.StatusBadRequest) + return + } + + // Convert to number to avoid parameter injection + endpoint, err := strconv.Atoi(param) + if err != nil { + renderError(w, r.URL.Path, "failed to read endpoint ID", http.StatusBadRequest) + return + } + + stdout, _, err := runCommand(ciliumPath, nil, "bpf", "policy", "get", strconv.Itoa(endpoint), "-ojson") + if err != nil { + renderError(w, r.URL.Path, "failed to read BPF map", http.StatusInternalServerError) + return + } + + renderJSON(w, r.URL.Path, stdout, http.StatusOK) +} + +func subMain() error { + socketClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, + } + + server := http.Server{ + Addr: ":8080", + Handler: nil, + } + + http.HandleFunc("/v1/endpoint/", handleEndpoint) + http.HandleFunc("/v1/identity/", handleIdentity) + http.HandleFunc("/policy/", handlePolicy) + + return server.ListenAndServe() +} diff --git a/cmd/cilium-agent-proxy/main.go b/cmd/cilium-agent-proxy/main.go new file mode 100644 index 0000000..e3f0b6d --- /dev/null +++ b/cmd/cilium-agent-proxy/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/cybozu-go/network-policy-viewer/cmd/cilium-agent-proxy/app" + +func main() { + app.Execute() +} diff --git a/cmd/dump.go b/cmd/npv/app/dump.go similarity index 98% rename from cmd/dump.go rename to cmd/npv/app/dump.go index 8c0b274..bb93769 100644 --- a/cmd/dump.go +++ b/cmd/npv/app/dump.go @@ -1,4 +1,4 @@ -package cmd +package app import ( "bytes" diff --git a/cmd/helper.go b/cmd/npv/app/helper.go similarity index 99% rename from cmd/helper.go rename to cmd/npv/app/helper.go index 7f0c40f..9f39101 100644 --- a/cmd/helper.go +++ b/cmd/npv/app/helper.go @@ -1,4 +1,4 @@ -package cmd +package app import ( "context" diff --git a/cmd/list.go b/cmd/npv/app/list.go similarity index 94% rename from cmd/list.go rename to cmd/npv/app/list.go index fd884d9..0ab21c3 100644 --- a/cmd/list.go +++ b/cmd/npv/app/list.go @@ -1,4 +1,4 @@ -package cmd +package app import ( "context" @@ -76,12 +76,12 @@ func parseDerivedFromEntry(input []string, direction string) derivedFromEntry { func runList(ctx context.Context, w io.Writer, name string) error { _, dynamicClient, client, err := createClients(ctx, name) if err != nil { - return err + return fmt.Errorf("failed to create clients: %w", err) } endpointID, err := getPodEndpointID(ctx, dynamicClient, rootOptions.namespace, name) if err != nil { - return err + return fmt.Errorf("failed to get pod endpoint ID: %w", err) } params := endpoint.GetEndpointIDParams{ @@ -90,7 +90,7 @@ func runList(ctx context.Context, w io.Writer, name string) error { } response, err := client.Endpoint.GetEndpointID(¶ms) if err != nil { - return err + return fmt.Errorf("failed to get endpoint information: %w", err) } // The same rule appears multiple times in the response, so we need to dedup it diff --git a/cmd/root.go b/cmd/npv/app/root.go similarity index 98% rename from cmd/root.go rename to cmd/npv/app/root.go index d97d6a8..d46738f 100644 --- a/cmd/root.go +++ b/cmd/npv/app/root.go @@ -1,4 +1,4 @@ -package cmd +package app import ( "fmt" diff --git a/cmd/npv/main.go b/cmd/npv/main.go new file mode 100644 index 0000000..110745d --- /dev/null +++ b/cmd/npv/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/cybozu-go/network-policy-viewer/cmd/npv/app" + +func main() { + app.Execute() +} diff --git a/e2e/Makefile b/e2e/Makefile index d80f725..56e9a16 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -29,6 +29,10 @@ start: --namespace kube-system \ --set image.pullPolicy=IfNotPresent \ --set ipam.mode=kubernetes + + cd ..; docker build . -t cilium-agent-proxy:dev + kind load docker-image cilium-agent-proxy:dev + kustomize build testdata | kubectl apply -f - $(MAKE) --no-print-directory wait-for-workloads diff --git a/e2e/testdata/cilium-agent-proxy.yaml b/e2e/testdata/cilium-agent-proxy.yaml index 80344a7..0727244 100644 --- a/e2e/testdata/cilium-agent-proxy.yaml +++ b/e2e/testdata/cilium-agent-proxy.yaml @@ -15,15 +15,13 @@ spec: securityContext: fsGroup: 0 containers: - - image: ghcr.io/cybozu/envoy - name: envoy - command: ["envoy", "-c", "/etc/envoy/envoy-config.yaml"] - args: [] + - image: ghcr.io/cybozu-go/cilium-agent-proxy + name: proxy volumeMounts: - name: cilium-socket mountPath: /var/run/cilium - - name: envoy-config - mountPath: /etc/envoy + - name: bpf + mountPath: /sys/fs/bpf securityContext: capabilities: drop: @@ -32,6 +30,8 @@ spec: - name: cilium-socket hostPath: path: /var/run/cilium - - name: envoy-config - configMap: - name: cilium-agent-proxy + # "cilium bpf policy get" reads from /sys/fs/bpf + - name: bpf + hostPath: + path: /sys/fs/bpf + type: DirectoryOrCreate diff --git a/e2e/testdata/envoy-config.yaml b/e2e/testdata/envoy-config.yaml deleted file mode 100644 index 21fddfb..0000000 --- a/e2e/testdata/envoy-config.yaml +++ /dev/null @@ -1,49 +0,0 @@ -static_resources: - listeners: - - name: cilium-agent-proxy - address: - socket_address: - address: 0.0.0.0 - port_value: 8080 - filter_chains: - - filters: - - name: envoy.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress_http - http_filters: - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - route_config: - name: cilium-agent-proxy - virtual_hosts: - - name: cilium-agent-proxy - domains: ["*"] - routes: - - match: - prefix: "/v1/endpoint/" - headers: - - name: ":method" - string_match: - exact: "GET" - route: - cluster: cilium-agent-proxy - - match: - prefix: "/v1/identity/" - headers: - - name: ":method" - string_match: - exact: "GET" - route: - cluster: cilium-agent-proxy - clusters: - - name: cilium-agent-proxy - load_assignment: - cluster_name: cilium-agent-proxy - endpoints: - - lb_endpoints: - - endpoint: - address: - pipe: - path: /var/run/cilium/cilium.sock diff --git a/e2e/testdata/kustomization.yaml b/e2e/testdata/kustomization.yaml index 0ef3db1..a3e7cf8 100644 --- a/e2e/testdata/kustomization.yaml +++ b/e2e/testdata/kustomization.yaml @@ -3,11 +3,7 @@ kind: Kustomization resources: - cilium-agent-proxy.yaml - ubuntu.yaml -configMapGenerator: - - namespace: kube-system - name: cilium-agent-proxy - files: - - envoy-config.yaml images: - - name: ghcr.io/cybozu/envoy - newTag: 1.28.1.1 + - name: ghcr.io/cybozu-go/cilium-agent-proxy + newName: cilium-agent-proxy + newTag: dev diff --git a/main.go b/main.go deleted file mode 100644 index 8b925ad..0000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/cybozu-go/network-policy-viewer/cmd" -) - -func main() { - cmd.Execute() -}