| // Copyright 2019 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 agentendpoint |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "runtime" |
| "time" |
| |
| "cloud.google.com/go/storage" |
| "github.com/GoogleCloudPlatform/osconfig/agentconfig" |
| "github.com/GoogleCloudPlatform/osconfig/clog" |
| "github.com/GoogleCloudPlatform/osconfig/external" |
| "github.com/GoogleCloudPlatform/osconfig/util" |
| |
| "google.golang.org/api/option" |
| |
| "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" |
| ) |
| |
| var ( |
| winRoot = os.Getenv("SystemRoot") |
| sh = "/bin/sh" |
| |
| winPowershell string |
| winCmd string |
| |
| winPowershellArgs = []string{"-NonInteractive", "-NoProfile", "-ExecutionPolicy", "Bypass"} |
| |
| goos = runtime.GOOS |
| |
| errLinuxPowerShell = errors.New("interpreter POWERSHELL cannot be used on non-Windows system") |
| errWinNoInt = fmt.Errorf("an interpreter value of `SHELL` or `POWERSHELL` must be set for files ran on Windows systems") |
| ) |
| |
| func init() { |
| if winRoot == "" { |
| winRoot = `C:\Windows` |
| } |
| winPowershell = filepath.Join(winRoot, `System32\WindowsPowerShell\v1.0\PowerShell.exe`) |
| winCmd = filepath.Join(winRoot, `System32\cmd.exe`) |
| } |
| |
| var run = func(cmd *exec.Cmd) ([]byte, error) { |
| return cmd.CombinedOutput() |
| } |
| |
| func getGCSObject(ctx context.Context, bkt, obj string, gen int64) (string, error) { |
| cl, err := storage.NewClient(ctx, option.WithUniverseDomain(agentconfig.UniverseDomain())) |
| |
| if err != nil { |
| return "", fmt.Errorf("error creating gcs client: %v", err) |
| } |
| defer cl.Close() |
| reader, err := external.FetchGCSObject(ctx, cl, bkt, obj, gen) |
| if err != nil { |
| return "", fmt.Errorf("error fetching GCS object: %v", err) |
| } |
| defer reader.Close() |
| clog.Debugf(ctx, "Fetched GCS object bucket %s object %s generation number %d", bkt, obj, gen) |
| |
| localPath := filepath.Join(os.TempDir(), path.Base(obj)) |
| if _, err := util.AtomicWriteFileStream(reader, "", localPath, 0755); err != nil { |
| return "", fmt.Errorf("error downloading GCS object: %s", err) |
| } |
| |
| clog.Debugf(ctx, "Downloaded to local path %s", localPath) |
| return localPath, nil |
| } |
| |
| func executeCommand(ctx context.Context, path string, args []string) (int32, error) { |
| clog.Debugf(ctx, "Running command %s with args %s", path, args) |
| |
| cmd := exec.Command(path, args...) |
| out, err := run(cmd) |
| var exitCode int32 |
| if cmd.ProcessState != nil { |
| exitCode = int32(cmd.ProcessState.ExitCode()) |
| clog.Infof(ctx, "Command exit code: %d, out:\n%s", exitCode, out) |
| } |
| if err != nil { |
| if _, ok := err.(*exec.ExitError); !ok { |
| return -1, err |
| } |
| } |
| |
| return exitCode, nil |
| } |
| |
| type execTask struct { |
| StartedAt time.Time `json:",omitempty"` |
| client *Client |
| Task *execStepTask |
| TaskID string |
| } |
| |
| type execStepTask struct { |
| *agentendpointpb.ExecStepTask |
| } |
| |
| func (e *execTask) reportCompletedState(ctx context.Context, errMsg string, output *agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput) error { |
| req := &agentendpointpb.ReportTaskCompleteRequest{ |
| TaskId: e.TaskID, |
| TaskType: agentendpointpb.TaskType_EXEC_STEP_TASK, |
| ErrorMessage: errMsg, |
| Output: output, |
| } |
| if err := e.client.reportTaskComplete(ctx, req); err != nil { |
| return fmt.Errorf("error reporting completed state: %v", err) |
| } |
| return nil |
| } |
| |
| func (e *execTask) run(ctx context.Context) error { |
| clog.Infof(ctx, "Beginning ExecStepTask") |
| e.StartedAt = time.Now() |
| req := &agentendpointpb.ReportTaskProgressRequest{ |
| TaskId: e.TaskID, |
| TaskType: agentendpointpb.TaskType_EXEC_STEP_TASK, |
| Progress: &agentendpointpb.ReportTaskProgressRequest_ExecStepTaskProgress{ |
| ExecStepTaskProgress: &agentendpointpb.ExecStepTaskProgress{State: agentendpointpb.ExecStepTaskProgress_STARTED}, |
| }, |
| } |
| res, err := e.client.reportTaskProgress(ctx, req) |
| if err != nil { |
| return fmt.Errorf("error reporting state %s: %v", agentendpointpb.ExecStepTaskProgress_STARTED, err) |
| } |
| |
| if res.GetTaskDirective() == agentendpointpb.TaskDirective_STOP { |
| return e.reportCompletedState(ctx, errServerCancel.Error(), &agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput{ |
| ExecStepTaskOutput: &agentendpointpb.ExecStepTaskOutput{State: agentendpointpb.ExecStepTaskOutput_CANCELLED}, |
| }) |
| } |
| |
| stepConfig := e.Task.GetExecStep().GetLinuxExecStepConfig() |
| if goos == "windows" { |
| stepConfig = e.Task.GetExecStep().GetWindowsExecStepConfig() |
| } |
| |
| if stepConfig == nil { |
| // The given ExecTask does not apply to this OS. |
| return e.reportCompletedState(ctx, "", &agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput{ |
| ExecStepTaskOutput: &agentendpointpb.ExecStepTaskOutput{ |
| State: agentendpointpb.ExecStepTaskOutput_COMPLETED, |
| ExitCode: 0, |
| }, |
| }) |
| } |
| |
| localPath := stepConfig.GetLocalPath() |
| if gcsObject := stepConfig.GetGcsObject(); gcsObject != nil { |
| var err error |
| localPath, err = getGCSObject(ctx, gcsObject.GetBucket(), gcsObject.GetObject(), gcsObject.GetGenerationNumber()) |
| if err != nil { |
| msg := fmt.Sprintf("Error downloading GCS object: %v", err) |
| clog.Errorf(ctx, "%v", msg) |
| return e.reportCompletedState(ctx, msg, &agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput{ |
| ExecStepTaskOutput: &agentendpointpb.ExecStepTaskOutput{ |
| State: agentendpointpb.ExecStepTaskOutput_COMPLETED, |
| ExitCode: -1, |
| }, |
| }) |
| } |
| defer func() { |
| if err := os.Remove(localPath); err != nil { |
| clog.Errorf(ctx, "error removing downloaded file %s", err) |
| } |
| }() |
| } |
| |
| exitCode := int32(-1) |
| switch stepConfig.GetInterpreter() { |
| case agentendpointpb.ExecStepConfig_INTERPRETER_UNSPECIFIED: |
| if goos == "windows" { |
| err = errWinNoInt |
| } else { |
| exitCode, err = executeCommand(ctx, localPath, nil) |
| } |
| case agentendpointpb.ExecStepConfig_SHELL: |
| if goos == "windows" { |
| exitCode, err = executeCommand(ctx, winCmd, []string{"/c", localPath}) |
| } else { |
| exitCode, err = executeCommand(ctx, sh, []string{localPath}) |
| } |
| case agentendpointpb.ExecStepConfig_POWERSHELL: |
| if goos == "windows" { |
| exitCode, err = executeCommand(ctx, winPowershell, append(winPowershellArgs, "-File", localPath)) |
| } else { |
| err = errLinuxPowerShell |
| } |
| default: |
| err = fmt.Errorf("invalid interpreter %q", stepConfig.GetInterpreter()) |
| } |
| if err != nil { |
| msg := fmt.Sprintf("Error running ExecStepTask: %v", err) |
| clog.Errorf(ctx, "%v", msg) |
| return e.reportCompletedState(ctx, msg, &agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput{ |
| ExecStepTaskOutput: &agentendpointpb.ExecStepTaskOutput{ |
| State: agentendpointpb.ExecStepTaskOutput_COMPLETED, |
| ExitCode: exitCode, |
| }, |
| }) |
| } |
| |
| if err := e.reportCompletedState(ctx, "", &agentendpointpb.ReportTaskCompleteRequest_ExecStepTaskOutput{ |
| ExecStepTaskOutput: &agentendpointpb.ExecStepTaskOutput{ |
| State: agentendpointpb.ExecStepTaskOutput_COMPLETED, |
| ExitCode: exitCode, |
| }, |
| }); err != nil { |
| return err |
| } |
| clog.Infof(ctx, "Successfully completed ApplyConfigTask") |
| return nil |
| } |
| |
| // RunExecStep runs an ExecStepTask. |
| func (c *Client) RunExecStep(ctx context.Context, task *agentendpointpb.Task) error { |
| ctx = clog.WithLabels(ctx, task.GetServiceLabels()) |
| e := &execTask{ |
| TaskID: task.GetTaskId(), |
| client: c, |
| Task: &execStepTask{task.GetExecStepTask()}, |
| } |
| |
| return e.run(ctx) |
| } |