config/exec_resource.go (195 lines of code) (raw):
// Copyright 2020 Google Inc. 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 config
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/osconfig/clog"
"github.com/GoogleCloudPlatform/osconfig/util"
"cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
)
const maxExecOutputSize = 500 * 1024
var runner = util.CommandRunner(&util.DefaultRunner{})
type execResource struct {
*agentendpointpb.OSPolicy_Resource_ExecResource
validatePath, enforcePath, tempDir string
enforceOutput []byte
}
// TODO: use a persistent cache for downloaded files so we dont need to redownload them each time
func (e *execResource) download(ctx context.Context, execR *agentendpointpb.OSPolicy_Resource_ExecResource_Exec) (string, error) {
tmpDir, err := ioutil.TempDir(e.tempDir, "")
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %s", err)
}
// File extensions are important on Windows.
var name string
perms := os.FileMode(0644)
switch execR.GetSource().(type) {
case *agentendpointpb.OSPolicy_Resource_ExecResource_Exec_Script:
switch execR.GetInterpreter() {
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE:
if goos == "windows" {
name = "script.cmd"
} else {
name = "script"
}
perms = os.FileMode(0755)
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_SHELL:
if goos == "windows" {
name = "script.cmd"
} else {
name = "script.sh"
}
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_POWERSHELL:
name = "script.ps1"
default:
return "", fmt.Errorf("unsupported interpreter %q", execR.GetInterpreter())
}
name = filepath.Join(tmpDir, name)
if _, err := util.AtomicWriteFileStream(strings.NewReader(execR.GetScript()), "", name, perms); err != nil {
return "", err
}
case *agentendpointpb.OSPolicy_Resource_ExecResource_Exec_File:
if execR.GetFile().GetLocalPath() != "" {
return execR.GetFile().GetLocalPath(), nil
}
switch {
case execR.GetFile().GetGcs().GetObject() != "":
name = path.Base(execR.GetFile().GetGcs().GetObject())
case execR.GetFile().GetRemote().GetUri() != "":
name = path.Base(execR.GetFile().GetRemote().GetUri())
default:
return "", fmt.Errorf("unsupported File %v", execR.GetFile())
}
if execR.GetInterpreter() == agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE {
perms = os.FileMode(0755)
}
name = filepath.Join(tmpDir, name)
if _, err := downloadFile(ctx, name, perms, execR.GetFile()); err != nil {
return "", err
}
default:
return "", fmt.Errorf("unrecognized Source type for ExecResource: %q", execR.GetSource())
}
return name, nil
}
func (e *execResource) validate(ctx context.Context) (*ManagedResources, error) {
tmpDir, err := ioutil.TempDir("", "osconfig_exec_resource_")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %s", err)
}
e.tempDir = tmpDir
e.validatePath, err = e.download(ctx, e.GetValidate())
if err != nil {
return nil, err
}
// Assume lack of Enforce means policy is in VALIDATE mode.
if e.GetEnforce() != nil {
e.enforcePath, err = e.download(ctx, e.GetEnforce())
if err != nil {
return nil, err
}
}
return nil, nil
}
func (e *execResource) run(ctx context.Context, name string, execR *agentendpointpb.OSPolicy_Resource_ExecResource_Exec) ([]byte, []byte, int, error) {
if execR == nil {
return nil, nil, 0, fmt.Errorf("ExecResource Exec cannot be nil")
}
var cmd string
var args []string
switch execR.GetInterpreter() {
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE:
cmd = name
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_SHELL:
if goos == "windows" {
cmd = name
} else {
args = append(args, name)
cmd = "/bin/sh"
}
case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_POWERSHELL:
if goos != "windows" {
return nil, nil, 0, fmt.Errorf("interpreter %q can only be used on Windows systems", execR.GetInterpreter())
}
args = append(args, "-File", name)
cmd = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\PowerShell.exe"
default:
return nil, nil, 0, fmt.Errorf("unsupported interpreter %q", execR.GetInterpreter())
}
args = append(args, execR.GetArgs()...)
stdout, stderr, err := runner.Run(ctx, exec.CommandContext(ctx, cmd, args...))
code := 0
if err != nil {
code = -1
if v, ok := err.(*exec.ExitError); ok {
code = v.ExitCode()
}
}
return stdout, stderr, code, err
}
func (e *execResource) checkState(ctx context.Context) (inDesiredState bool, err error) {
// For validate we expect an exit code of 100 for "correct state" and 101 for "incorrect state".
// 100 was chosen over 0 (and 101 vs 1) because we want an explicit indicator of
// "correct" vs "incorrect" state and errors. Also Powershell will always exit 0 unless "exit"
// is explicitly called.
// A code of -1 indicates some other error, so we just return err.
stdout, stderr, code, err := e.run(ctx, e.validatePath, e.GetValidate())
switch code {
case -1:
return false, err
case 100:
return true, nil
case 101:
return false, nil
default:
return false, fmt.Errorf("unexpected return code from validate: %d, stdout: %s, stderr: %s", code, stdout, stderr)
}
}
func (e *execResource) enforceState(ctx context.Context) (inDesiredState bool, err error) {
clog.Infof(ctx, `Running "Enforce" for ExecResource.`)
// For enforce we expect an exit code of 100 for "success" and anything positive code is a failure".
// 100 was chosen over 0 because we want an explicit indicator of "sucess" vs errors.
// Also Powershell will always exit 0 unless "exit" is explicitly called.
// A code of -1 indicates some other error, so we just return err.
stdout, stderr, code, err := e.run(ctx, e.enforcePath, e.GetEnforce())
switch code {
case -1:
return false, err
case 100:
out, err := execOutput(ctx, e.GetEnforce().GetOutputFilePath())
e.enforceOutput = out
return true, err
default:
return false, fmt.Errorf("unexpected return code from enforce: %d, stdout: %s, stderr: %s", code, stdout, stderr)
}
}
func execOutput(ctx context.Context, outputFilePath string) ([]byte, error) {
if outputFilePath == "" {
return nil, nil
}
clog.Debugf(ctx, "Reading %q for ExecResource output", outputFilePath)
f, err := os.Open(outputFilePath)
if err != nil {
return nil, fmt.Errorf("error opening OutputFilePath: %v", err)
}
defer f.Close()
// Make a byte slice with a capacity of 1 over maxSize (for checking maxExecOutputSize).
output := make([]byte, 0, maxExecOutputSize+1)
// Read up to capactity.
n, err := f.Read(output[:cap(output)])
output = output[:len(output)+n]
if err != nil && err != io.EOF {
return output, fmt.Errorf("error reading from OutputFilePath: %v", err)
}
// Return the output up to this point and an error if total size is greater than maxExecOutputSize.
if n > maxExecOutputSize {
return output[:maxExecOutputSize], fmt.Errorf("contents of OutputFilePath greater than %dK", maxExecOutputSize/1024)
}
return output, nil
}
func (e *execResource) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) {
if e.enforceOutput != nil {
rCompliance.Output = &agentendpointpb.OSPolicyResourceCompliance_ExecResourceOutput_{
ExecResourceOutput: &agentendpointpb.OSPolicyResourceCompliance_ExecResourceOutput{
EnforcementOutput: e.enforceOutput,
},
}
}
}
func (e *execResource) cleanup(ctx context.Context) error {
if e.tempDir != "" {
return os.RemoveAll(e.tempDir)
}
return nil
}