Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

refactor(docker): docker provider to provider v2 #1384

Merged
merged 1 commit into from
Mar 7, 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
57 changes: 57 additions & 0 deletions provider/v2/docker/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 docker

import (
"fmt"

"github.com/spf13/viper"
)

const (
DefaultEnvPrefix = "VMCLARITY_DOCKER"
DefaultHelperImage = "alpine:3.18.2"
DefaultNetworkName = "vmclarity"
)

type Config struct {
// HelperImage defines helper container image that performs init tasks.
HelperImage string `mapstructure:"helper_image"`
// NetworkName defines the user defined bridge network where we attach the scanner container.
NetworkName string `mapstructure:"network_name"`
}

func NewConfig() (*Config, error) {
// Avoid modifying the global instance
v := viper.New()

v.SetEnvPrefix(DefaultEnvPrefix)
v.AllowEmptyEnv(true)
v.AutomaticEnv()

_ = v.BindEnv("helper_image")
v.SetDefault("helper_image", DefaultHelperImage)

_ = v.BindEnv("network_name")
v.SetDefault("network_name", DefaultNetworkName)

config := &Config{}
if err := v.Unmarshal(config); err != nil {
return nil, fmt.Errorf("failed to parse provider configuration. Provider=Docker: %w", err)
}

return config, nil
}
123 changes: 123 additions & 0 deletions provider/v2/docker/discoverer/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 discoverer

import (
"context"
"fmt"
"strings"
"sync"
"time"

"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup"

apitypes "github.com/openclarity/vmclarity/api/types"
"github.com/openclarity/vmclarity/core/log"
"github.com/openclarity/vmclarity/core/to"
)

func (d *Discoverer) getContainerAssets(ctx context.Context) ([]apitypes.AssetType, error) {
logger := log.GetLoggerFromContextOrDiscard(ctx)

// List all docker containers
containers, err := d.DockerClient.ContainerList(ctx, containertypes.ListOptions{All: true})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}

// Results will be written to assets concurrently
assetMu := sync.Mutex{}
assets := make([]apitypes.AssetType, 0, len(containers))

// Process each container in an independent processor goroutine
processGroup, processCtx := errgroup.WithContext(ctx)
for _, container := range containers {
processGroup.Go(
// processGroup expects a function with empty signature, so we use a function
// generator to enable adding arguments. This avoids issues when using loop
// variables in goroutines via shared memory space.
//
// If any processor returns an error, it will stop all processors.
// IDEA: Decide what the acceptance criteria should be (e.g. >= 50% container processed)
func(container types.Container) func() error {
return func() error {
// Get container info
info, err := d.getContainerInfo(processCtx, container.ID)
if err != nil {
logger.Warnf("Failed to get container. id=%v: %v", container.ID, err)
return nil // skip fail
}

// Convert to asset
asset := apitypes.AssetType{}
err = asset.FromContainerInfo(info)
if err != nil {
return fmt.Errorf("failed to create AssetType from ContainerInfo: %w", err)
}

// Write to assets
assetMu.Lock()
assets = append(assets, asset)
assetMu.Unlock()

return nil
}
}(container),
)
}

// This will block until all the processors have executed successfully or until
// the first error. If an error is returned by any processors, processGroup will
// cancel execution via processCtx and return that error.
err = processGroup.Wait()
if err != nil {
return nil, fmt.Errorf("failed to process containers: %w", err)
}

return assets, nil
}

func (d *Discoverer) getContainerInfo(ctx context.Context, containerID string) (apitypes.ContainerInfo, error) {
// Inspect container
info, err := d.DockerClient.ContainerInspect(ctx, containerID)
if err != nil {
return apitypes.ContainerInfo{}, fmt.Errorf("failed to inspect container: %w", err)
}

createdAt, err := time.Parse(time.RFC3339, info.Created)
if err != nil {
return apitypes.ContainerInfo{}, fmt.Errorf("failed to parse time: %w", err)
}

// Get container image info
imageInfo, err := d.getContainerImageInfo(ctx, info.Image)
if err != nil {
return apitypes.ContainerInfo{}, err
}

containerName := strings.Trim(info.Name, "/")

return apitypes.ContainerInfo{
ContainerName: &containerName,
CreatedAt: to.Ptr(createdAt),
ContainerID: containerID,
Image: to.Ptr(imageInfo),
Labels: convertTags(info.Config.Labels),
ObjectType: "ContainerInfo",
}, nil
}
78 changes: 78 additions & 0 deletions provider/v2/docker/discoverer/discoverer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 discoverer

import (
"context"

"github.com/docker/docker/client"

apitypes "github.com/openclarity/vmclarity/api/types"
"github.com/openclarity/vmclarity/provider"
)

var _ provider.Discoverer = &Discoverer{}

type Discoverer struct {
DockerClient *client.Client
}

func (d *Discoverer) DiscoverAssets(ctx context.Context) provider.AssetDiscoverer {
assetDiscoverer := provider.NewSimpleAssetDiscoverer()

go func() {
defer close(assetDiscoverer.OutputChan)

// Get image assets
imageAssets, err := d.getImageAssets(ctx)
if err != nil {
assetDiscoverer.Error = provider.FatalErrorf("failed to get images. Provider=%s: %w", apitypes.Docker, err)
return
}

// Get container assets
containerAssets, err := d.getContainerAssets(ctx)
if err != nil {
assetDiscoverer.Error = provider.FatalErrorf("failed to get containers. Provider=%s: %w", apitypes.Docker, err)
return
}

// Combine assets
assets := append(imageAssets, containerAssets...)

for _, asset := range assets {
select {
case assetDiscoverer.OutputChan <- asset:
case <-ctx.Done():
assetDiscoverer.Error = ctx.Err()
return
}
}
}()

return assetDiscoverer
}

func convertTags(tags map[string]string) *[]apitypes.Tag {
ret := make([]apitypes.Tag, 0, len(tags))
for key, val := range tags {
ret = append(ret, apitypes.Tag{
Key: key,
Value: val,
})
}
return &ret
}
109 changes: 109 additions & 0 deletions provider/v2/docker/discoverer/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 discoverer

import (
"context"
"fmt"
"sync"

"github.com/docker/docker/api/types"
imagetypes "github.com/docker/docker/api/types/image"
"golang.org/x/sync/errgroup"

apitypes "github.com/openclarity/vmclarity/api/types"
"github.com/openclarity/vmclarity/core/log"
"github.com/openclarity/vmclarity/core/to"
)

func (d *Discoverer) getImageAssets(ctx context.Context) ([]apitypes.AssetType, error) {
logger := log.GetLoggerFromContextOrDiscard(ctx)

// List all docker images
images, err := d.DockerClient.ImageList(ctx, types.ImageListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list images: %w", err)
}

// Results will be written to assets concurrently
assetMu := sync.Mutex{}
assets := make([]apitypes.AssetType, 0, len(images))

// Process each image in an independent processor goroutine
processGroup, processCtx := errgroup.WithContext(ctx)
for _, image := range images {
processGroup.Go(
// processGroup expects a function with empty signature, so we use a function
// generator to enable adding arguments. This avoids issues when using loop
// variables in goroutines via shared memory space.
//
// If any processor returns an error, it will stop all processors.
// IDEA: Decide what the acceptance criteria should be (e.g. >= 50% images processed)
func(image imagetypes.Summary) func() error {
return func() error {
// Get container image info
info, err := d.getContainerImageInfo(processCtx, image.ID)
if err != nil {
logger.Warnf("Failed to get image. id=%v: %v", image.ID, err)
return nil // skip fail
}

// Convert to asset
asset := apitypes.AssetType{}
err = asset.FromContainerImageInfo(info)
if err != nil {
return fmt.Errorf("failed to create AssetType from ContainerImageInfo: %w", err)
}

// Write to assets
assetMu.Lock()
assets = append(assets, asset)
assetMu.Unlock()

return nil
}
}(image),
)
}

// This will block until all the processors have executed successfully or until
// the first error. If an error is returned by any processors, processGroup will
// cancel execution via processCtx and return that error.
err = processGroup.Wait()
if err != nil {
return nil, fmt.Errorf("failed to process images: %w", err)
}

return assets, nil
}

func (d *Discoverer) getContainerImageInfo(ctx context.Context, imageID string) (apitypes.ContainerImageInfo, error) {
image, _, err := d.DockerClient.ImageInspectWithRaw(ctx, imageID)
if err != nil {
return apitypes.ContainerImageInfo{}, fmt.Errorf("failed to inspect image: %w", err)
}

return apitypes.ContainerImageInfo{
Architecture: to.Ptr(image.Architecture),
ImageID: image.ID,
Labels: convertTags(image.Config.Labels),
RepoTags: to.Ptr(image.RepoTags),
RepoDigests: to.Ptr(image.RepoDigests),
ObjectType: "ContainerImageInfo",
Os: to.Ptr(image.Os),
Size: to.Ptr(image.Size),
}, nil
}
Loading
Loading