Skip to content

Commit

Permalink
Add job containers package
Browse files Browse the repository at this point in the history
* Add `JobContainer` and `JobProcess` types as the two types to represent a job container
and a process in a job container.
* Add logic to find the executable being asked to run for a job container.
* Logic to launch the container as specific user.
* Logic to mount the containers scratch space on the host to a directory.

Signed-off-by: Daniel Canter <dcanter@microsoft.com>
  • Loading branch information
dcantah committed Dec 23, 2020
1 parent 2010d9a commit 2f7eb8b
Show file tree
Hide file tree
Showing 16 changed files with 1,093 additions and 39 deletions.
408 changes: 408 additions & 0 deletions internal/jobcontainers/jobcontainer.go

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions internal/jobcontainers/logon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package jobcontainers

import (
"fmt"
"strings"

"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/pkg/errors"
"golang.org/x/sys/windows"
)

// Takes in a DOMAIN\Username or just Username and will return a token
// for the account if successful.
func processToken(user string) (windows.Token, error) {
var (
domain string
userName string
token windows.Token
)

split := strings.Split(user, "\\")
if len(split) == 2 {
domain = split[0]
userName = split[1]
} else if len(split) == 1 {
userName = split[0]
} else {
return token, fmt.Errorf("invalid user string `%s`", user)
}

// If empty, ContainerUser or ContainerAdministrator just let it inherit the token
// from whatever is used to launch it (containerd-shim etc). Regular container images
// are usable for job containers so we need to handle the ContainerUser and ContainerAdministrator
// cases.
if user == "" || user == "ContainerUser" || user == "ContainerAdministrator" {
return openCurrentProcessToken()
}

var logonType uint32
if domain == "NT AUTHORITY" {
// User asking to run as a local system account (NETWORK SERVICE, LOCAL SERVICE, SYSTEM)
logonType = winapi.LOGON32_LOGON_SERVICE
} else {
// They want a user account, use the interactive logon type instead of service
logonType = winapi.LOGON32_LOGON_INTERACTIVE
}

if err := winapi.LogonUser(
windows.StringToUTF16Ptr(userName),
windows.StringToUTF16Ptr(domain),
nil,
logonType,
winapi.LOGON32_PROVIDER_DEFAULT,
&token,
); err != nil {
return token, errors.Wrap(err, "failed to logon user")
}
return token, nil
}

func openCurrentProcessToken() (windows.Token, error) {
var token windows.Token
if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ALL_ACCESS, &token); err != nil {
return 0, errors.Wrap(err, "failed to open current process token")
}
return token, nil
}
77 changes: 77 additions & 0 deletions internal/jobcontainers/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package jobcontainers

import (
"context"
"fmt"

"github.com/Microsoft/hcsshim/internal/jobobject"
"github.com/Microsoft/hcsshim/internal/processorinfo"

"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/oci"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)

// This file contains helpers for converting parts of the oci spec to useful
// structures/limits to be applied to a job object.

// Oci spec to job object limit information. Will do any conversions to job object specific values from
// their respective OCI representations. E.g. we convert CPU count into the correct job object cpu
// rate value internally.
func specToLimits(ctx context.Context, s *specs.Spec) (*jobobject.JobLimits, error) {
// CPU limits
cpuNumSet := 0
cpuCount := uint32(oci.ParseAnnotationsCPUCount(ctx, s, oci.AnnotationContainerProcessorCount, 0))
if cpuCount > 0 {
cpuNumSet++
}

cpuLimit := uint32(oci.ParseAnnotationsCPULimit(ctx, s, oci.AnnotationContainerProcessorLimit, 0))
if cpuLimit > 0 {
cpuNumSet++
}

cpuWeight := uint32(oci.ParseAnnotationsCPUWeight(ctx, s, oci.AnnotationContainerProcessorWeight, 0))
if cpuWeight > 0 {
cpuNumSet++
}

if cpuNumSet > 1 {
return nil, fmt.Errorf("invalid spec - Windows Job Container CPU Count: '%d', Limit: '%d', and Weight: '%d' are mutually exclusive", cpuCount, cpuLimit, cpuWeight)
} else if cpuNumSet == 1 {
if cpuCount != 0 {
hostCPUCount := uint32(processorinfo.ProcessorCount())
if cpuCount > hostCPUCount {
log.G(ctx).WithFields(logrus.Fields{
"requested": cpuCount,
"assigned": hostCPUCount,
}).Warn("Changing user requested CPUCount to current number of processors")
cpuCount = hostCPUCount
}
// Job object API does not support "CPU count". Instead, we translate the notion of "count" into
// CPU limit, which represents the amount of the host system's processors that the job can use to
// a percentage times 100. For example, to let the job use 20% of the available LPs the rate would
// be 20 times 100, or 2,000.
cpuLimit = calculateJobCPURate(hostCPUCount, cpuCount)
} else if cpuWeight != 0 {
cpuWeight = calculateJobCPUWeight(cpuWeight)
}
// Nothing to do for cpu limit, we can assign directly.
}

// Memory limit
memLimit := oci.ParseAnnotationsMemory(ctx, s, oci.AnnotationContainerMemorySizeInMB, 0)

// IO limits
maxBandwidth := int64(oci.ParseAnnotationsStorageBps(ctx, s, oci.AnnotationContainerStorageQoSBandwidthMaximum, 0))
maxIops := int64(oci.ParseAnnotationsStorageIops(ctx, s, oci.AnnotationContainerStorageQoSIopsMaximum, 0))

return &jobobject.JobLimits{
CPULimit: cpuLimit,
CPUWeight: cpuWeight,
MaxIOPS: maxIops,
MaxBandwidth: maxBandwidth,
MemoryLimitInBytes: memLimit * 1024 * 1024, // ParseAnnotationsMemory value is returned in MB
}, nil
}
57 changes: 57 additions & 0 deletions internal/jobcontainers/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package jobcontainers

import (
"errors"
"os"
"os/exec"
"path/filepath"
)

// Checks the sandbox volume and PATH env variable to find application name. If the path
// is relative, E.g. path\to\binary, it will be searched for in the sandbox volume. If it
// is solely an application name then the base directory of the sandbox volume and all directories
// in PATH will be searched. Job containers can run anything on the host so C:\path\to\exe,
// D:\path\to\exe etc. are just passed through as is.
func findExecutable(path string, imagePath string) (string, error) {
// We need to check if the users actually provided a file extension for the binary. If not, just
// append .exe to it and start the search.
if len(path) >= 4 {
if path[len(path)-4:] != ".exe" {
path += ".exe"
}
}

// Absolute path, just return the path. User specified (hopefully)
// a path to an executable on the host. C:\path\to\binary.exe
if filepath.IsAbs(path) {
return path, nil
}

// Not an absolute path, if no path seperators search in image path and if this fails
// search in PATH. If both of these fail then we error out. Otherwise if there are path
// seperators treat this as the user is trying to run an executable from the image directory
// and append the directory to the path supplied.
//
// E.g. path\to\binary.exe ---> C:\path\to\sandbox\ + path\to\binary.exe.
if filepath.Base(path) == path {
// User specified just the application name E.g. name_of_binary or name_of_binary.exe
// Check payload directory first and if this fails check PATH.
absPath := filepath.Join(imagePath, path)
if _, err := os.Stat(absPath); err == nil {
return absPath, nil
}

// Error in searching in the payload path, try PATH.
if absPath, err := exec.LookPath(path); err == nil {
return absPath, nil
}
} else {
// This is a relative path E.g path\to\binary.exe. Append the image directory to
// it and hope it's there.
absPath := filepath.Join(imagePath, path)
if _, err := os.Stat(absPath); err == nil {
return absPath, nil
}
}
return "", errors.New("failed to find executable on the system")
}
70 changes: 70 additions & 0 deletions internal/jobcontainers/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package jobcontainers

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)

func TestFindExecutable(t *testing.T) {
var (
testExecutable = "test.exe"
executablePath = `/path/to/binary`
)

if _, err := findExecutable("ping", ""); err != nil {
t.Fatalf("failed to find executable: %s", err)
}

if _, err := findExecutable("ping.exe", ""); err != nil {
t.Fatalf("failed to find executable: %s", err)
}

if _, err := findExecutable("C:\\windows\\system32\\ping", ""); err != nil {
t.Fatalf("failed to find executable: %s", err)
}

if _, err := findExecutable("C:\\windows\\system32\\ping.exe", ""); err != nil {
t.Fatalf("failed to find executable: %s", err)
}

// Create nested directory structure with blank test executables.
path, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("failed to make temporary directory: %s", err)
}
defer os.RemoveAll(path)

_, err = os.Create(filepath.Join(path, testExecutable))
if err != nil {
t.Fatalf("failed to create test executable: %s", err)
}

nestedPath := filepath.Join(path, executablePath)
if err := os.MkdirAll(nestedPath, 0700); err != nil {
t.Fatalf("failed to create nested directory structure: %s", err)
}

nestedExe := filepath.Join(nestedPath, testExecutable)
_, err = os.Create(nestedExe)
if err != nil {
t.Fatalf("failed to create test executable: %s", err)
}

if testPath, err := findExecutable(testExecutable, path); err != nil {
t.Fatalf("failed to find executable: %s", err)
} else {
if testPath != filepath.Join(path, testExecutable) {
t.Fatalf("test executable location does not match, expected `%s` and received `%s`", filepath.Join(path, testExecutable), testPath)
}
}

if testPath, err := findExecutable(nestedExe, path); err != nil {
t.Fatalf("failed to find executable: %s", err)
} else {
if testPath != nestedExe {
t.Fatalf("test executable location does not match, expected `%s` and received `%s`", nestedExe, testPath)
}
}
}
Loading

0 comments on commit 2f7eb8b

Please sign in to comment.