Skip to content

Commit

Permalink
Add a container loader for AppNet Agent
Browse files Browse the repository at this point in the history
  • Loading branch information
aws-gibbskt committed Jul 6, 2022
1 parent d19d0a4 commit ddabb46
Show file tree
Hide file tree
Showing 9 changed files with 676 additions and 0 deletions.
52 changes: 52 additions & 0 deletions agent/serviceconnect/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

// https://golang.org/src/syscall/zerrors_linux_386.go#L1382
const noSuchFile = "no such file or directory"

// UnsupportedPlatformError indicates an error when loading appnet container
// image on an unsupported OS platform
type UnsupportedPlatformError struct {
error
}

// IsUnsupportedPlatform returns true if the error is of UnsupportedPlatformError
// type
func IsUnsupportedPlatform(err error) bool {
_, ok := err.(UnsupportedPlatformError)
return ok
}

// NewUnsupportedPlatformError creates a new UnsupportedPlatformError object
func NewUnsupportedPlatformError(err error) UnsupportedPlatformError {
return UnsupportedPlatformError{err}
}

// NoSuchFileError wraps the error from the os package with the message
// "no such file error"
type NoSuchFileError struct {
error
}

// NewNoSuchFileError creates a new NoSuchFileError object
func NewNoSuchFileError(err error) NoSuchFileError {
return NoSuchFileError{err}
}

// IsNoSuchFileError returns true if the error is of NoSuchFileError type
func IsNoSuchFileError(err error) bool {
_, ok := err.(NoSuchFileError)
return ok
}
52 changes: 52 additions & 0 deletions agent/serviceconnect/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build unit
// +build unit

// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

import (
"errors"
"fmt"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

func TestUnsupportedPlatform(t *testing.T) {
testCases := map[error]bool{
errors.New("error"): false,
NewUnsupportedPlatformError(errors.New("error")): true,
}

for err, expected := range testCases {
t.Run(fmt.Sprintf("returns %t for type %s", expected, reflect.TypeOf(err)), func(t *testing.T) {
assert.Equal(t, expected, IsUnsupportedPlatform(err))
})
}
}

func TestIsNoSuchFileError(t *testing.T) {
testCases := map[error]bool{
errors.New("error"): false,
NewNoSuchFileError(errors.New("No such file")): true,
}

for err, expected := range testCases {
t.Run(fmt.Sprintf("return %t for type %s", expected, reflect.TypeOf(err)), func(t *testing.T) {
assert.Equal(t, expected, IsNoSuchFileError(err))
})
}
}
16 changes: 16 additions & 0 deletions agent/serviceconnect/generate_mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

//go:generate mockgen -destination=mocks/load_mocks.go -copyright_file=../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/serviceconnect Loader
82 changes: 82 additions & 0 deletions agent/serviceconnect/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

import (
"context"
"fmt"

"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
"github.com/aws/amazon-ecs-agent/agent/logger"
"github.com/aws/amazon-ecs-agent/agent/logger/field"
"github.com/docker/docker/api/types"
)

var (
defaultAgentContainerImageName = "appnet_agent"
defaultAgentContainerTag = "service_connect.v1"
)

// Loader defines an interface for loading the appnetAgent container image. This is mostly
// to facilitate mocking and testing of the LoadImage method
type Loader interface {
LoadImage(ctx context.Context, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error)
IsLoaded(dockerClient dockerapi.DockerClient) (bool, error)
}

type loader struct {
AgentContainerImageName string
AgentContainerTag string
AgentContainerTarballPath string
}

// New creates a new AppNet Agent image loader
func New() Loader {
return &loader{
AgentContainerImageName: defaultAgentContainerImageName,
AgentContainerTag: defaultAgentContainerTag,
AgentContainerTarballPath: defaultAgentContainerTarballPath,
}
}

// This function uses the DockerClient to inspect the image with the given name and tag.
func getAgentContainerImage(name string, tag string, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) {
imageName := fmt.Sprintf("%s:%s", name, tag)
logger.Debug("Inspecting appnet agent container image:", logger.Fields{
field.Image: imageName,
})

image, err := dockerClient.InspectImage(imageName)
if err != nil {
return nil, fmt.Errorf("appnet agent container load: failed to inspect image: %s; %w", imageName, err)
}

return image, nil
}

// Common function for linux and windows to check if the container appnet Agent image has been loaded
func (agent *loader) isImageLoaded(dockerClient dockerapi.DockerClient) (bool, error) {
image, err := getAgentContainerImage(
agent.AgentContainerImageName, agent.AgentContainerTag, dockerClient)

if err != nil {
return false, err
}

if image == nil || image.ID == "" {
return false, nil
}

return true, nil
}
70 changes: 70 additions & 0 deletions agent/serviceconnect/load_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//go:build linux
// +build linux

// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

import (
"context"
"fmt"
"os"

"github.com/aws/amazon-ecs-agent/agent/dockerclient"
"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
"github.com/aws/amazon-ecs-agent/agent/logger"
"github.com/aws/amazon-ecs-agent/agent/logger/field"

"github.com/docker/docker/api/types"
)

var (
defaultAgentContainerTarballPath = "/managed-agents/serviceconnect/appnet_agent.interface-v1.tar"
)

// LoadImage helps load the AppNetAgent container image for the agent
func (agent *loader) LoadImage(ctx context.Context, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) {
logger.Debug("Loading appnet agent container tarball:", logger.Fields{
field.Image: agent.AgentContainerTarballPath,
})
if err := loadFromFile(ctx, agent.AgentContainerTarballPath, dockerClient); err != nil {
return nil, err
}

return getAgentContainerImage(
agent.AgentContainerImageName, agent.AgentContainerTag, dockerClient)
}

func (agent *loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) {
return agent.isImageLoaded(dockerClient)
}

var open = os.Open

func loadFromFile(ctx context.Context, path string, dockerClient dockerapi.DockerClient) error {
containerReader, err := open(path)
if err != nil {
if err.Error() == noSuchFile {
return NewNoSuchFileError(fmt.Errorf(
"appnet agent container load: failed to read container image: %s : %w", path, err))
}
return fmt.Errorf("appnet agent container load: failed to read container image: %s : %w", path, err)
}
if err := dockerClient.LoadImage(ctx, containerReader, dockerclient.LoadImageTimeout); err != nil {
return fmt.Errorf("appnet agent container load: failed to load container image: %s : %w", path, err)
}

return nil

}
122 changes: 122 additions & 0 deletions agent/serviceconnect/load_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//go:build linux && unit
// +build linux,unit

// Copyright Amazon.com Inc. or 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. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 serviceconnect

import (
"context"
"errors"
"os"
"testing"

"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks"
mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks"

"github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

const (
containerTarballPath = "/path/to/container.tar"
)

func mockOpen() func() {
open = func(name string) (*os.File, error) {
return nil, nil
}
return func() {
open = os.Open
}
}

// TestLoadFromFileWithReaderError tests loadFromFile with reader error
func TestLoadFromFileWithReaderError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Docker SDK tests
mockDockerSDK := mock_sdkclient.NewMockClient(ctrl)
mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil)
sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl)
sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil)

ctx, cancel := context.WithCancel(context.TODO())
defer cancel()

client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx)
assert.NoError(t, err)

open = func(name string) (*os.File, error) {
return nil, errors.New("Dummy Reader Error")
}
defer func() {
open = os.Open
}()

err = loadFromFile(ctx, containerTarballPath, client)
assert.Error(t, err)
}

// TestLoadFromFileHappyPath tests loadFromFile against happy path
func TestLoadFromFileHappyPath(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Docker SDK tests
mockDockerSDK := mock_sdkclient.NewMockClient(ctrl)
mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil)
sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl)
sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil)

ctx, cancel := context.WithCancel(context.TODO())
defer cancel()

client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx)
assert.NoError(t, err)
mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{}, nil)
defer mockOpen()()

err = loadFromFile(ctx, containerTarballPath, client)
assert.NoError(t, err)
}

// TestLoadFromFileDockerLoadImageError tests loadFromFile against error
// from Docker clients LoadImage
func TestLoadFromFileDockerLoadImageError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Docker SDK tests
mockDockerSDK := mock_sdkclient.NewMockClient(ctrl)
mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil)
sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl)
sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil)

ctx, cancel := context.WithCancel(context.TODO())
defer cancel()

client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx)
assert.NoError(t, err)
mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{},
errors.New("Dummy Load Image Error"))

defer mockOpen()()

err = loadFromFile(ctx, containerTarballPath, client)
assert.Error(t, err)
}
Loading

0 comments on commit ddabb46

Please sign in to comment.