//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 app

import (
	"os"

	asmfactory "github.com/aws/amazon-ecs-agent/agent/asm/factory"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
	ebs "github.com/aws/amazon-ecs-agent/agent/ebs"
	"github.com/aws/amazon-ecs-agent/agent/ecscni"
	"github.com/aws/amazon-ecs-agent/agent/engine"
	"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
	"github.com/aws/amazon-ecs-agent/agent/eni/watcher"
	"github.com/aws/amazon-ecs-agent/agent/gpu"
	s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
	ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory"
	"github.com/aws/amazon-ecs-agent/agent/statechange"
	"github.com/aws/amazon-ecs-agent/agent/taskresource"
	cgroup "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control"
	"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
	"github.com/aws/amazon-ecs-agent/ecs-agent/credentials"

	"github.com/aws/aws-sdk-go-v2/service/ecs/types"
	"github.com/cihub/seelog"
	"github.com/pkg/errors"
)

// initPID defines the process identifier for the init process
const initPID = 1

// awsVPCCNIPlugins is a list of CNI plugins required by the ECS Agent
// to configure the ENI for a task
var awsVPCCNIPlugins = []string{
	ecscni.VPCENIPluginName,
	ecscni.ECSBridgePluginName,
	ecscni.ECSIPAMPluginName,
	ecscni.ECSAppMeshPluginName,
	ecscni.ECSBranchENIPluginName,
}

// startWindowsService is not supported on Linux
func (agent *ecsAgent) startWindowsService() int {
	seelog.Error("Windows Services are not supported on Linux")
	return 1
}

var getPid = os.Getpid

// initializeTaskENIDependencies initializes all of the dependencies required by
// the Agent to support the 'awsvpc' networking mode. A non nil error is returned
// if an error is encountered during this process. An additional boolean flag to
// indicate if this error is considered terminal is also returned
func (agent *ecsAgent) initializeTaskENIDependencies(state dockerstate.TaskEngineState, taskEngine engine.TaskEngine) (error, bool) {
	// Check if the Agent process's pid  == 1, which means it's running without an init system
	if getPid() == initPID {
		// This is a terminal error. Bad things happen with invoking the
		// the ENI plugin when there's no init process in the pid namespace.
		// Specifically, the DHClient processes that are started as children
		// of the Agent will not be reaped leading to the ENI device
		// disappearing until the Agent is killed.
		return errors.New("agent is not started with an init system"), true
	}

	// Set VPC and Subnet IDs for the instance
	if err, ok := agent.setVPCSubnet(); err != nil {
		return err, ok
	}

	// Validate that the CNI plugins exist in the expected path and that
	// they possess the right capabilities
	if err := agent.verifyCNIPluginsCapabilities(); err != nil {
		// An error here is terminal as it means that the plugins
		// do not support the ENI capability
		return err, true
	}

	if err := agent.startENIWatcher(state, taskEngine.StateChangeEvents()); err != nil {
		// If udev watcher was not initialized in this run because of the udev socket
		// file not being available etc, the Agent might be able to retry and succeed
		// on the next run. Hence, returning a false here for terminal bool
		return err, false
	}

	return nil, false
}

// verifyCNIPluginsCapabilities returns an error if there's an error querying
// capabilities or if the required capability is absent from the capabilities
// of the following plugins:
// a. vpc-eni
// b. ecs-bridge
// c. ecs-ipam
// d. aws-appmesh
// e. vpc-branch-eni
func (agent *ecsAgent) verifyCNIPluginsCapabilities() error {
	// Check if we can get capabilities from each plugin
	for _, plugin := range awsVPCCNIPlugins {
		// skip verifying branch cni plugin if eni trunking is not enabled
		if plugin == ecscni.ECSBranchENIPluginName && agent.cfg != nil && !agent.cfg.ENITrunkingEnabled.Enabled() {
			continue
		}

		capabilities, err := agent.cniClient.Capabilities(plugin)
		if err != nil {
			return err
		}
		// appmesh plugin is not needed for awsvpc networking capability
		if plugin == ecscni.ECSAppMeshPluginName {
			continue
		}
		if !contains(capabilities, ecscni.CapabilityAWSVPCNetworkingMode) {
			return errors.Errorf("plugin '%s' doesn't support the capability: %s",
				plugin, ecscni.CapabilityAWSVPCNetworkingMode)
		}
	}

	return nil
}

// startENIWatcher starts the udev monitor and the watcher for receiving
// notifications from the monitor
func (agent *ecsAgent) startENIWatcher(state dockerstate.TaskEngineState, stateChangeEvents chan<- statechange.Event) error {
	seelog.Debug("Setting up ENI Watcher")
	if agent.eniWatcher == nil {
		eniWatcher, err := watcher.New(agent.ctx, agent.mac, state, stateChangeEvents)
		if err != nil {
			return errors.Wrapf(err, "unable to create ENI watcher")
		}
		agent.eniWatcher = eniWatcher

		if err := agent.eniWatcher.Init(); err != nil {
			return errors.Wrapf(err, "unable to initialize eni watcher")
		}
		go agent.eniWatcher.Start()
	}
	return nil
}

func (agent *ecsAgent) startEBSWatcher(
	state dockerstate.TaskEngineState,
	taskEngine engine.TaskEngine,
	dockerClient dockerapi.DockerClient,
) {
	if agent.ebsWatcher == nil {
		seelog.Debug("Creating new EBS watcher...")
		agent.ebsWatcher = ebs.NewWatcher(agent.ctx, agent.cfg, state, taskEngine, dockerClient)
		go agent.ebsWatcher.Start()
	}
}

// initializeResourceFields exists mainly for testing doStart() to use mock Control
// object
func (agent *ecsAgent) initializeResourceFields(credentialsManager credentials.Manager) {
	ec2InstanceID, _ := agent.getEc2MetadataWithRetry(agent.ec2MetadataClient.InstanceID, "EC2 instance ID")
	agent.resourceFields = &taskresource.ResourceFields{
		Control: cgroup.New(),
		ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{
			IOUtil:             ioutilwrapper.NewIOUtil(),
			ASMClientCreator:   asmfactory.NewClientCreator(),
			SSMClientCreator:   ssmfactory.NewSSMClientCreator(),
			S3ClientCreator:    s3factory.NewS3ClientCreator(),
			CredentialsManager: credentialsManager,
			EC2InstanceID:      ec2InstanceID,
		},
		Ctx:              agent.ctx,
		DockerClient:     agent.dockerClient,
		NvidiaGPUManager: gpu.NewNvidiaGPUManager(),
	}
}

func (agent *ecsAgent) cgroupInit() error {
	err := agent.resourceFields.Control.Init()
	// When task CPU and memory limits are enabled, all tasks are placed
	// under the '/ecs' cgroup root.
	if err == nil {
		return nil
	}
	if agent.cfg.TaskCPUMemLimit.Value == config.ExplicitlyEnabled {
		return errors.Wrapf(err, "unable to setup '/ecs' cgroup")
	}
	seelog.Warnf("Disabling TaskCPUMemLimit because agent is unable to setup '/ecs' cgroup: %v", err)
	agent.cfg.TaskCPUMemLimit.Value = config.ExplicitlyDisabled
	return nil
}

func (agent *ecsAgent) initializeGPUManager() error {
	if agent.resourceFields != nil && agent.resourceFields.NvidiaGPUManager != nil {
		return agent.resourceFields.NvidiaGPUManager.Initialize()
	}
	return nil
}

func (agent *ecsAgent) getPlatformDevices() []types.PlatformDevice {
	if agent.cfg.GPUSupportEnabled {
		if agent.resourceFields != nil && agent.resourceFields.NvidiaGPUManager != nil {
			return agent.resourceFields.NvidiaGPUManager.GetDevices()
		}
	}
	return nil
}

func (agent *ecsAgent) loadPauseContainer() error {
	// Load the pause container's image from the 'disk'
	_, err := agent.pauseLoader.LoadImage(agent.ctx, agent.cfg, agent.dockerClient)

	return err
}
