-
Notifications
You must be signed in to change notification settings - Fork 256
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
16 changed files
with
1,093 additions
and
39 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.