| // 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" |
| "crypto/rand" |
| "crypto/rsa" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| "time" |
| |
| agentendpoint "cloud.google.com/go/osconfig/agentendpoint/apiv1" |
| "github.com/GoogleCloudPlatform/guest-logging-go/logger" |
| "golang.org/x/oauth2/jws" |
| "google.golang.org/api/option" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| "google.golang.org/grpc/test/bufconn" |
| |
| "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" |
| ) |
| |
| var testIDToken string |
| |
| func TestMain(m *testing.M) { |
| cs := &jws.ClaimSet{ |
| Exp: time.Now().Add(1 * time.Hour).Unix(), |
| } |
| key, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| fmt.Printf("Error creating rsa key: %v", err) |
| os.Exit(1) |
| } |
| testIDToken, err = jws.Encode(nil, cs, key) |
| if err != nil { |
| fmt.Printf("Error creating jwt token: %v", err) |
| os.Exit(1) |
| } |
| |
| ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprint(w, testIDToken) |
| })) |
| |
| if err := os.Setenv("GCE_METADATA_HOST", strings.Trim(ts.URL, "http://")); err != nil { |
| fmt.Printf("Error running os.Setenv: %v", err) |
| os.Exit(1) |
| } |
| |
| opts := logger.LogOpts{LoggerName: "OSConfigAgent", Debug: true, Writers: []io.Writer{os.Stdout}} |
| logger.Init(context.Background(), opts) |
| |
| out := m.Run() |
| ts.Close() |
| os.Exit(out) |
| } |
| |
| const bufSize = 1024 * 1024 |
| |
| type testClient struct { |
| client *Client |
| s *grpc.Server |
| } |
| |
| func (c *testClient) close() { |
| c.client.Close() |
| c.s.Stop() |
| } |
| |
| func newTestClient(ctx context.Context, srv agentendpointpb.AgentEndpointServiceServer) (*testClient, error) { |
| lis := bufconn.Listen(bufSize) |
| s := grpc.NewServer() |
| agentendpointpb.RegisterAgentEndpointServiceServer(s, srv) |
| |
| go func() { |
| if err := s.Serve(lis); err != nil { |
| log.Fatalf("Server exited with error: %v", err) |
| } |
| }() |
| |
| var bufDialer = func(string, time.Duration) (net.Conn, error) { |
| return lis.Dial() |
| } |
| |
| conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithDialer(bufDialer), grpc.WithInsecure()) |
| if err != nil { |
| return nil, err |
| } |
| |
| client, err := agentendpoint.NewClient(ctx, option.WithGRPCConn(conn)) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &testClient{ |
| client: &Client{raw: client, noti: make(chan struct{}, 1)}, |
| s: s, |
| }, nil |
| } |
| |
| type agentEndpointServiceTestServer struct { |
| streamClose chan struct{} |
| streamSend chan struct{} |
| permissionError chan struct{} |
| taskStart bool |
| execTaskProgress bool |
| patchTaskProgress bool |
| applyConfigTaskProgress bool |
| execTaskComplete bool |
| patchTaskComplete bool |
| applyConfigTaskComplete bool |
| runTaskIDs []string |
| } |
| |
| func newAgentEndpointServiceTestServer() *agentEndpointServiceTestServer { |
| return &agentEndpointServiceTestServer{ |
| streamClose: make(chan struct{}, 1), |
| streamSend: make(chan struct{}, 1), |
| permissionError: make(chan struct{}, 1), |
| } |
| } |
| |
| func (s *agentEndpointServiceTestServer) ReceiveTaskNotification(req *agentendpointpb.ReceiveTaskNotificationRequest, srv agentendpointpb.AgentEndpointService_ReceiveTaskNotificationServer) error { |
| for { |
| select { |
| case <-s.streamClose: |
| return nil |
| case <-s.streamSend: |
| srv.Send(&agentendpointpb.ReceiveTaskNotificationResponse{}) |
| case <-s.permissionError: |
| return status.Errorf(codes.PermissionDenied, "") |
| } |
| } |
| } |
| |
| func (s *agentEndpointServiceTestServer) StartNextTask(ctx context.Context, req *agentendpointpb.StartNextTaskRequest) (*agentendpointpb.StartNextTaskResponse, error) { |
| // We first return an TaskType_EXEC_STEP_TASK, then TaskType_APPLY_PATCHES, then TaskType_APPLY_CONFIG_TASK. |
| // After all tasks complete, we return nothing signalling the end to tasks. |
| s.taskStart = true |
| switch { |
| case s.applyConfigTaskComplete && s.execTaskComplete && s.patchTaskComplete: |
| return &agentendpointpb.StartNextTaskResponse{}, nil |
| case !s.execTaskComplete: |
| return &agentendpointpb.StartNextTaskResponse{Task: &agentendpointpb.Task{TaskType: agentendpointpb.TaskType_EXEC_STEP_TASK, TaskId: "TaskType_EXEC_STEP_TASK"}}, nil |
| case !s.patchTaskComplete: |
| return &agentendpointpb.StartNextTaskResponse{Task: &agentendpointpb.Task{TaskType: agentendpointpb.TaskType_APPLY_PATCHES, TaskId: "TaskType_APPLY_PATCHES"}}, nil |
| case !s.applyConfigTaskComplete: |
| return &agentendpointpb.StartNextTaskResponse{Task: &agentendpointpb.Task{TaskType: agentendpointpb.TaskType_APPLY_CONFIG_TASK, TaskId: "TaskType_APPLY_CONFIG_TASK"}}, nil |
| default: |
| return &agentendpointpb.StartNextTaskResponse{}, status.Errorf(codes.Unimplemented, "unexpected start next task") |
| } |
| } |
| |
| func (s *agentEndpointServiceTestServer) ReportTaskProgress(ctx context.Context, req *agentendpointpb.ReportTaskProgressRequest) (*agentendpointpb.ReportTaskProgressResponse, error) { |
| // Simply record and send STOP. |
| switch req.GetTaskType() { |
| case agentendpointpb.TaskType_EXEC_STEP_TASK: |
| s.execTaskProgress = true |
| case agentendpointpb.TaskType_APPLY_PATCHES: |
| s.patchTaskProgress = true |
| case agentendpointpb.TaskType_APPLY_CONFIG_TASK: |
| s.applyConfigTaskProgress = true |
| default: |
| return &agentendpointpb.ReportTaskProgressResponse{}, status.Errorf(codes.Unimplemented, "task type %q not implemented", req.GetTaskType()) |
| } |
| return &agentendpointpb.ReportTaskProgressResponse{TaskDirective: agentendpointpb.TaskDirective_STOP}, nil |
| } |
| |
| func (s *agentEndpointServiceTestServer) ReportTaskComplete(ctx context.Context, req *agentendpointpb.ReportTaskCompleteRequest) (*agentendpointpb.ReportTaskCompleteResponse, error) { |
| // Record what task types we have seen, when the complete is called for TaskType_APPLY_CONFIG_TASK, close the stream. |
| s.runTaskIDs = append(s.runTaskIDs, req.GetTaskId()) |
| switch req.GetTaskType() { |
| case agentendpointpb.TaskType_EXEC_STEP_TASK: |
| s.execTaskComplete = true |
| case agentendpointpb.TaskType_APPLY_PATCHES: |
| s.patchTaskComplete = true |
| case agentendpointpb.TaskType_APPLY_CONFIG_TASK: |
| s.applyConfigTaskComplete = true |
| default: |
| return &agentendpointpb.ReportTaskCompleteResponse{}, status.Errorf(codes.Unimplemented, "task type %q not implemented", req.GetTaskType()) |
| } |
| if s.execTaskComplete && s.patchTaskComplete && s.applyConfigTaskComplete { |
| s.streamClose <- struct{}{} |
| } |
| return &agentendpointpb.ReportTaskCompleteResponse{}, nil |
| } |
| |
| func (*agentEndpointServiceTestServer) RegisterAgent(ctx context.Context, req *agentendpointpb.RegisterAgentRequest) (*agentendpointpb.RegisterAgentResponse, error) { |
| return nil, status.Errorf(codes.Unimplemented, "method RegisterAgent not implemented") |
| } |
| |
| func (*agentEndpointServiceTestServer) ReportInventory(ctx context.Context, req *agentendpointpb.ReportInventoryRequest) (*agentendpointpb.ReportInventoryResponse, error) { |
| return nil, status.Errorf(codes.Unimplemented, "method ReportInventory not implemented") |
| } |
| |
| func TestWaitForTask(t *testing.T) { |
| ctx := context.Background() |
| srv := newAgentEndpointServiceTestServer() |
| tc, err := newTestClient(ctx, srv) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer tc.close() |
| |
| td, err := ioutil.TempDir(os.TempDir(), "") |
| if err != nil { |
| t.Fatalf("error creating temp dir: %v", err) |
| } |
| defer os.RemoveAll(td) |
| taskStateFile = filepath.Join(td, "testState") |
| |
| // Stream recieve. |
| srv.streamSend <- struct{}{} |
| if err := tc.client.waitForTask(ctx); err != nil { |
| t.Errorf("did not expect error from a closed stream: %v", err) |
| } |
| if !srv.execTaskProgress { |
| t.Error("expected ReportTaskProgress for TaskType_EXEC_STEP_TASK to have been called") |
| } |
| if !srv.execTaskComplete { |
| t.Error("expected ReportTaskComplete for TaskType_EXEC_STEP_TASK to have been called") |
| } |
| if !srv.patchTaskProgress { |
| t.Error("expected ReportTaskProgress for TaskType_APPLY_PATCHES to have been called") |
| } |
| if !srv.patchTaskComplete { |
| t.Error("expected ReportTaskComplete for TaskType_APPLY_PATCHES to have been called") |
| } |
| if !srv.applyConfigTaskProgress { |
| t.Error("expected ReportTaskProgress for TaskType_APPLY_CONFIG_TASK to have been called") |
| } |
| if !srv.applyConfigTaskComplete { |
| t.Error("expected ReportTaskComplete for TaskType_APPLY_CONFIG_TASK to have been called") |
| } |
| } |
| |
| func TestWaitForTaskErrors(t *testing.T) { |
| ctx := context.Background() |
| srv := newAgentEndpointServiceTestServer() |
| tc, err := newTestClient(ctx, srv) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // errServiceNotEnabled from PermissionDenied error. |
| srv.permissionError <- struct{}{} |
| if err := tc.client.waitForTask(ctx); !errors.Is(err, errServiceNotEnabled) { |
| t.Errorf("did not get expected errServiceNotEnabled, got: %v", err) |
| } |
| |
| // No error from a closed stream. |
| srv.streamClose <- struct{}{} |
| if err := tc.client.waitForTask(ctx); err != nil { |
| t.Errorf("did not expect error from a closed stream: %v", err) |
| } |
| } |
| |
| func TestLoadPatchTaskFromState(t *testing.T) { |
| ctx := context.Background() |
| srv := newAgentEndpointServiceTestServer() |
| tc, err := newTestClient(ctx, srv) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer tc.close() |
| |
| td, err := ioutil.TempDir(os.TempDir(), "") |
| if err != nil { |
| t.Fatalf("error creating temp dir: %v", err) |
| } |
| defer os.RemoveAll(td) |
| taskStateFile = filepath.Join(td, "testState") |
| |
| srv.streamSend <- struct{}{} |
| |
| // No state. |
| if err := tc.client.loadTaskFromState(ctx); err != nil { |
| t.Error(err) |
| } |
| if srv.taskStart { |
| t.Error("expected ReportTaskStart to not have been called") |
| } |
| |
| // Bad state. |
| if err := ioutil.WriteFile(taskStateFile, []byte("bad"), 0600); err != nil { |
| t.Fatal(err) |
| } |
| if err := tc.client.loadTaskFromState(ctx); err == nil { |
| t.Error("expected error from loadTaskFromState") |
| } |
| |
| // Existing task. |
| taskID := "foo" |
| if err := ioutil.WriteFile(taskStateFile, []byte(fmt.Sprintf(`{"PatchTask":{"TaskID":"%s", "PatchStep": "%s"}}`, taskID, patching)), 0600); err != nil { |
| t.Fatal(err) |
| } |
| if err := tc.client.loadTaskFromState(ctx); err != nil { |
| t.Fatal(err) |
| } |
| |
| srv.execTaskComplete = true |
| srv.applyConfigTaskComplete = true |
| // Launch another patch task, this should run AFTER the task loaded from state file |
| if err := tc.client.waitForTask(ctx); err != nil { |
| t.Errorf("did not expect error from a closed stream: %v", err) |
| } |
| |
| if srv.taskStart { |
| t.Error("did not expect ReportTaskStart to have been called") |
| } |
| if !srv.patchTaskProgress { |
| t.Error("expected ReportTaskProgress for TaskType_APPLY_PATCHES to have been called") |
| } |
| if !srv.patchTaskComplete { |
| t.Error("expected ReportTaskComplete for TaskType_APPLY_PATCHES to have been called") |
| } |
| if len(srv.runTaskIDs) != 1 { |
| t.Fatalf("expected srv.runTaskIDs to have a length of 1, not %d", len(srv.runTaskIDs)) |
| } |
| if srv.runTaskIDs[0] != taskID { |
| t.Errorf("first entry in runTaskIDs does not match taskID, %q, %q", srv.runTaskIDs, taskID) |
| } |
| } |