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

feat(k8s): Discover container images and running containers #711

Merged
merged 10 commits into from
Dec 4, 2023
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
8 changes: 4 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ linters-settings:
- github.com/docker/docker
- oras.land/oras-go
# End-to-end testing
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
- github.com/cucumber/godog
- cloud.google.com/go
- go.opentelemetry.io/otel
- go.opentelemetry.io/otel/metric
- go.opentelemetry.io/otel/sdk
# Viper
- github.com/mitchellh/mapstructure

Expand Down
46 changes: 46 additions & 0 deletions Dockerfile.cr-discovery-server
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# syntax=docker/dockerfile:1

# xx is a helper for cross-compilation
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.3.0@sha256:904fe94f236d36d65aeb5a2462f88f2c537b8360475f6342e7599194f291fb7e AS xx

FROM --platform=$BUILDPLATFORM golang:1.21.4-bullseye AS builder

COPY --from=xx / /

ARG TARGETPLATFORM

RUN --mount=type=cache,id=${TARGETPLATFORM}-apt,target=/var/cache/apt,sharing=locked \
apt-get update \
&& xx-apt-get install -y --no-install-recommends \
gcc \
libc6-dev

WORKDIR /build

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=bind,source=.,target=/build,ro \
xx-go mod download -x

ARG VERSION
ARG BUILD_TIMESTAMP
ARG COMMIT_HASH

ENV CGO_ENABLED=0

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=bind,source=.,target=/build,ro \
xx-go build -ldflags="-s -w -extldflags -static \
-X 'github.com/openclarity/vmclarity/pkg/version.Version=${VERSION}' \
-X 'github.com/openclarity/vmclarity/pkg/version.CommitHash=${COMMIT_HASH}' \
-X 'github.com/openclarity/vmclarity/pkg/version.BuildTimestamp=${BUILD_TIMESTAMP}'" \
-o /bin/vmclarity-cr-discovery-server cmd/vmclarity-cr-discovery-server/main.go

RUN xx-verify /bin/vmclarity-cr-discovery-server

FROM alpine:3.18

COPY --from=builder ["/bin/vmclarity-cr-discovery-server", "/bin/vmclarity-cr-discovery-server"]

ENTRYPOINT ["/bin/vmclarity-cr-discovery-server"]
23 changes: 20 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ VMCLARITY_ORCHESTRATOR_IMAGE = $(DOCKER_REGISTRY)/vmclarity-orchestrator:$(DOCKE
VMCLARITY_UI_IMAGE = $(DOCKER_REGISTRY)/vmclarity-ui:$(DOCKER_TAG)
VMCLARITY_UIBACKEND_IMAGE = $(DOCKER_REGISTRY)/vmclarity-ui-backend:$(DOCKER_TAG)
VMCLARITY_SCANNER_IMAGE = $(DOCKER_REGISTRY)/vmclarity-cli:$(DOCKER_TAG)
VMCLARITY_CR_DISCOVERY_SERVER_IMAGE = $(DOCKER_REGISTRY)/vmclarity-cr-discovery-server:$(DOCKER_TAG)

####
## Load additional makefiles
Expand All @@ -60,7 +61,7 @@ help: ## Display this help
build: ui build-all-go ## Build all components

.PHONY: build-all-go
build-all-go: bin/vmclarity-apiserver bin/vmclarity-cli bin/vmclarity-orchestrator bin/vmclarity-ui-backend ## Build all go components
build-all-go: bin/vmclarity-apiserver bin/vmclarity-cli bin/vmclarity-orchestrator bin/vmclarity-ui-backend bin/vmclarity-cr-discovery-server ## Build all go components

bin/vmclarity-orchestrator: $(shell find api) $(shell find cmd/vmclarity-orchestrator) $(shell find pkg) go.mod go.sum | $(BIN_DIR)
go build -race -ldflags="-s -w \
Expand Down Expand Up @@ -90,6 +91,9 @@ bin/vmclarity-ui-backend: $(shell find api) $(shell find cmd/vmclarity-ui-backen
-X 'github.com/openclarity/vmclarity/pkg/version.BuildTimestamp=$(BUILD_TIMESTAMP)'" \
-o $@ cmd/vmclarity-ui-backend/main.go

bin/vmclarity-cr-discovery-server: $(shell find api) $(shell find cmd/vmclarity-cr-discovery-server) $(shell find pkg) go.mod go.sum | $(BIN_DIR)
go build -race -o bin/vmclarity-cr-discovery-server cmd/vmclarity-cr-discovery-server/main.go

.PHONY: clean
clean: clean-ui clean-go ## Clean all build artifacts

Expand Down Expand Up @@ -188,7 +192,7 @@ test: ## Run Go unit tests
##@ Docker

.PHONY: docker
docker: docker-apiserver docker-cli docker-orchestrator docker-ui docker-ui-backend ## Build All Docker images
docker: docker-apiserver docker-cli docker-orchestrator docker-ui docker-ui-backend docker-cr-discovery-server ## Build All Docker images

.PHONY: docker-apiserver
docker-apiserver: ## Build API Server container image
Expand Down Expand Up @@ -233,8 +237,16 @@ docker-ui-backend: ## Build UI Backend container image
--build-arg COMMIT_HASH=$(COMMIT_HASH) \
-t $(VMCLARITY_UIBACKEND_IMAGE) .

.PHONY: docker-cr-discovery-server
docker-cr-discovery-server: ## Build K8S Image Resolver Docker image
$(info Building cr-discovery-server docker image ...)
docker build --file ./Dockerfile.cr-discovery-server --build-arg VERSION=$(VERSION) \
--build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \
--build-arg COMMIT_HASH=$(COMMIT_HASH) \
-t $(VMCLARITY_CR_DISCOVERY_SERVER_IMAGE) .

.PHONY: push-docker
push-docker: push-docker-apiserver push-docker-cli push-docker-orchestrator push-docker-ui push-docker-ui-backend ## Build and push all container images
push-docker: push-docker-apiserver push-docker-cli push-docker-orchestrator push-docker-ui push-docker-ui-backend push-docker-cr-discovery-server ## Build and Push All Docker images

.PHONY: push-docker-apiserver
push-docker-apiserver: docker-apiserver ## Build and push API Server container image
Expand All @@ -261,6 +273,11 @@ push-docker-ui-backend: docker-ui-backend ## Build and push UI Backend container
$(info Publishing ui-backend docker image ...)
docker push $(DOCKER_IMAGE)-ui-backend:$(DOCKER_TAG)

.PHONY: push-docker-cr-discovery-server
push-docker-cr-discovery-server: docker-cr-discovery-server ## Build and Push K8S Image Resolver Docker image
@echo "Publishing cr-discovery-server docker image ..."
docker push $(DOCKER_IMAGE)-cr-discovery-server:$(DOCKER_TAG)

##@ Code generation

.PHONY: gen
Expand Down
97 changes: 97 additions & 0 deletions api/models/containerimageinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

package models

import (
"fmt"
"strings"
)

// GetFirstRepoTag returns the first repo tag if it exists. Otherwise, returns false.
func (c *ContainerImageInfo) GetFirstRepoTag() (string, bool) {
var tag string
Expand All @@ -38,3 +43,95 @@ func (c *ContainerImageInfo) GetFirstRepoDigest() (string, bool) {

return digest, ok
}

// Merge merges target and c together and then returns the merged result. c is
// not a pointer so that:
// a) a non-pointer ContainerImageInfo can be merged.
// b) the source ContainerimageInfo can not be modified by this function.
func (c ContainerImageInfo) Merge(target ContainerImageInfo) (ContainerImageInfo, error) {
id, err := CoalesceComparable(c.ImageID, target.ImageID)
if err != nil {
return c, fmt.Errorf("failed to merge Id field: %w", err)
}

// NOTE(sambetts) Size seems to depend on the host and container
// runtime; there doesn't seem to be a clean way to get a repeatable
// size. If the sizes conflict we'll pick the larger of the two sizes
// as it is likely to be more accurate to the real size not taking into
// account deduplication in the CRI etc. The sizes are normally within
// a few kilobytes of each other.
size, err := CoalesceComparable(*c.Size, *target.Size)
if err != nil {
if *c.Size > *target.Size {
size = *c.Size
} else {
size = *target.Size
}
}

os, err := CoalesceComparable(*c.Os, *target.Os)
if err != nil {
return c, fmt.Errorf("failed to merge Os field: %w", err)
}

architecture, err := CoalesceComparable(*c.Architecture, *target.Architecture)
if err != nil {
return c, fmt.Errorf("failed to merge Architecture field: %w", err)
}

labels := UnionSlices(*c.Labels, *target.Labels)

repoDigests := UnionSlices(*c.RepoDigests, *target.RepoDigests)

repoTags := UnionSlices(*c.RepoTags, *target.RepoTags)

return ContainerImageInfo{
ImageID: id,
Size: &size,
Labels: &labels,
Os: &os,
Architecture: &architecture,
RepoDigests: &repoDigests,
RepoTags: &repoTags,
}, nil
}

const nilString = "nil"

func (c ContainerImageInfo) String() string {
size := nilString
if c.Size != nil {
size = fmt.Sprintf("%d", *c.Size)
}

labels := nilString
if c.Labels != nil {
l := make([]string, len(*c.Labels))
for i, label := range *c.Labels {
l[i] = fmt.Sprintf("{Key: \"%s\", Value: \"%s\"}", label.Key, label.Value)
}
labels = fmt.Sprintf("[%s]", strings.Join(l, ", "))
}

os := nilString
if c.Os != nil {
os = *c.Os
}

architecture := nilString
if c.Architecture != nil {
architecture = *c.Architecture
}

repoDigests := nilString
if c.RepoDigests != nil {
repoDigests = fmt.Sprintf("[%s]", strings.Join(*c.RepoDigests, ", "))
}

repoTags := nilString
if c.RepoTags != nil {
repoTags = fmt.Sprintf("[%s]", strings.Join(*c.RepoTags, ", "))
}

return fmt.Sprintf("{ImageID: %s, Size: %s, Labels: %s, Arch: %s, OS: %s, Digests: %s, Tags: %s}", c.ImageID, size, labels, architecture, os, repoDigests, repoTags)
}
47 changes: 47 additions & 0 deletions api/models/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// 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 models

import "fmt"

// CoalesceComparable will return original if target is not set, and target if
// original is not set. If both are set then they must be the same otherwise an
// error is raised.
func CoalesceComparable[T comparable](original, target T) (T, error) {
var zero T
if original != zero && target != zero && original != target {
return zero, fmt.Errorf("%v does not match %v", original, target)
}
if original != zero {
return original, nil
}
return target, nil
}

// UnionSlices returns the union of the input slices.
func UnionSlices[T comparable](inputs ...[]T) []T {
seen := map[T]struct{}{}
result := []T{}
for _, i := range inputs {
for _, j := range i {
if _, ok := seen[j]; !ok {
seen[j] = struct{}{}
result = append(result, j)
}
}
}
return result
}
101 changes: 101 additions & 0 deletions cmd/vmclarity-cr-discovery-server/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// 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 cmd

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/openclarity/vmclarity/pkg/containerruntimediscovery"
"github.com/openclarity/vmclarity/pkg/shared/log"
)

var (
listenAddr string

// Base logger.
logger *logrus.Entry

rootCmd = &cobra.Command{
Use: "vmclarity-cr-discovery-server",
Short: "Runs a server which provides endpoints for querying the container runtime.",
Long: "Runs a server which provides endpoints for querying the container runtime.",
SilenceUsage: true, // Don't print the usage when an error is returned from RunE
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ctx = log.SetLoggerForContext(ctx, logger)

discoverer, err := containerruntimediscovery.NewDiscoverer(ctx)
if err != nil {
return fmt.Errorf("unable to create discoverer: %w", err)
}

abortCtx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()

crds := containerruntimediscovery.NewContainerRuntimeDiscoveryServer(listenAddr, discoverer)
crds.Serve(abortCtx)

logger.Infof("Server started listening on %s...", listenAddr)

<-abortCtx.Done()

logger.Infof("Shutting down...")

shutdownContext, cancel := context.WithTimeout(ctx, 30*time.Second) // nolint:gomnd
defer cancel()
err = crds.Shutdown(shutdownContext)
if err != nil {
return fmt.Errorf("failed to shutdown server: %w", err)
}

logger.Infof("Successfully Shutdown. Goodbye.")

return nil
},
}
)

// Execute executes the root command.
func Execute() error {
// nolint: wrapcheck
return rootCmd.Execute()
}

func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(
&listenAddr,
"listenAddr",
":8080",
"The address and port to run the discovery HTTP server on. If address is unspecified\n"+
"such as :8080 then the server will listen on all available IP addresses of the system.")

log.InitLogger(logrus.InfoLevel.String(), os.Stderr)
logger = logrus.WithField("app", "vmclarity")
}

func initConfig() {
viper.AutomaticEnv()
}
Loading