cmd/wrapper/main.go (196 lines of code) (raw):
// Copyright 2024 Google LLC.
//
// 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.
// Wrapper is the binary executed inside the test VM. It fetches the test
// binary, executes it, and uploads its results to GCS.
package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/url"
"os"
"os/exec"
"path"
"runtime"
"strings"
"time"
"cloud.google.com/go/storage"
"github.com/GoogleCloudPlatform/cloud-image-tests/utils"
vm_pb "github.com/GoogleCloudPlatform/cloud-image-tests/vm_test_info"
"google.golang.org/protobuf/proto"
)
// In special cases such as the shutdown script, the guest attribute match
// on the first boot must have a different name than the usual guest attribute.
func checkFirstBootSpecialGA(ctx context.Context) bool {
if _, err := utils.GetMetadata(ctx, "instance", "attributes", "shouldRebootDuringTest"); err == nil {
_, foundFirstBootGA := utils.GetMetadata(ctx, "instance", "guest-attributes",
utils.GuestAttributeTestNamespace, utils.FirstBootGAKey)
// if the special attribute to match the first boot of the shutdown script test is already set, foundFirstBootGA will be nil and we should use the regular guest attribute.
if foundFirstBootGA != nil {
return true
}
}
return false
}
func main() {
ctx := context.Background()
testTimeout, err := utils.GetMetadata(ctx, "instance", "attributes", "_cit_timeout")
if err != nil {
log.Fatalf("failed to get metadata _cit_timeout: %v", err)
}
d, err := time.ParseDuration(testTimeout)
if err != nil {
log.Fatalf("_cit_timeout %v is not a valid duration: %v", testTimeout, err)
}
ctx, cancel := context.WithTimeout(ctx, d)
defer cancel()
client, err := storage.NewClient(ctx)
if err != nil {
log.Fatalf("failed to create cloud storage client: %v", err)
}
log.Printf("FINISHED-BOOTING")
firstBootSpecialAttribute := checkFirstBootSpecialGA(ctx)
// firstBootSpecialGA should be true if we need to match a different guest attribute than the usual guest attribute
defer func(ctx context.Context, firstBootSpecialGA bool) {
var err error
if firstBootSpecialGA {
err = utils.PutMetadata(ctx, path.Join("instance", "guest-attributes", utils.GuestAttributeTestNamespace,
utils.FirstBootGAKey), "")
} else {
err = utils.PutMetadata(ctx, path.Join("instance", "guest-attributes", utils.GuestAttributeTestNamespace,
utils.GuestAttributeTestKey), "")
}
if err != nil {
log.Printf("could not place guest attribute key to end test: %v", err)
}
for f := 0; f < 5; f++ {
log.Printf("FINISHED-TEST")
time.Sleep(1 * time.Second)
}
}(ctx, firstBootSpecialAttribute)
testPackageURL, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_package_url")
if err != nil {
log.Fatalf("failed to get metadata _test_package_url: %v", err)
}
resultsURL, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_results_url")
if err != nil {
log.Fatalf("failed to get metadata _test_results_url: %v", err)
}
propertiesURL, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_properties_url")
if err != nil {
log.Fatalf("failed to get metadata _test_properties_url: %v", err)
}
var testArguments = []string{"-test.v", "-test.timeout", testTimeout}
testRun, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_run")
if err == nil && testRun != "" {
testArguments = append(testArguments, "-test.run", testRun)
}
testExcludeFilter, err := utils.GetMetadata(ctx, "instance", "attributes", "_exclude_discrete_tests")
if err == nil && testExcludeFilter != "" {
testArguments = append(testArguments, "-test.skip", testExcludeFilter)
}
testPackage, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_package_name")
if err != nil {
log.Fatalf("failed to get metadata _test_package_name: %v", err)
}
testSuiteName, err := utils.GetMetadata(ctx, "instance", "attributes", "_test_suite_name")
if err != nil {
log.Fatalf("failed to get metadata _test_suite_name: %v", err)
}
machineType, err := utils.GetMetadata(ctx, "instance", "machine-type")
if err != nil {
log.Fatalf("failed to get metadata _machine_type: %v", err)
}
zone, err := utils.GetMetadata(ctx, "instance", "zone")
if err != nil {
log.Fatalf("failed to get metadata _zone: %v", err)
}
id, err := utils.GetMetadata(ctx, "instance", "id")
if err != nil {
log.Fatalf("failed to get metadata _id: %v", err)
}
name, err := utils.GetMetadata(ctx, "instance", "name")
if err != nil {
log.Fatalf("failed to get metadata _name: %v", err)
}
workDirPath := "/etc/"
if runtime.GOOS == "windows" {
workDirPath = "C:\\"
}
workDir, err := os.MkdirTemp(workDirPath, "image_test")
if err != nil {
log.Fatalf("failed to create work dir: %v", err)
}
workDir = workDir + "/"
if err = utils.DownloadGCSObjectToFile(ctx, client, testPackageURL, workDir+testPackage); err != nil {
log.Fatalf("failed to download object: %v", err)
}
client.Close()
log.Printf("sleep 30s to allow environment to stabilize")
time.Sleep(30 * time.Second)
out, err := executeCmd(workDir+testPackage, workDir, testArguments)
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
log.Printf("test package exited with error: %v stderr: %q", ee, ee.Stderr)
} else {
log.Fatalf("failed to execute test package: %v stdout: %q", err, out)
}
}
log.Printf("command output:\n%s\n", out)
client, err = storage.NewClient(ctx)
if err != nil {
log.Fatalf("failed to create cloud storage client: %v", err)
}
defer client.Close()
if err = uploadGCSObject(ctx, client, resultsURL, bytes.NewReader(out)); err != nil {
log.Fatalf("failed to upload test result: %v", err)
}
vmInfoProto := &vm_pb.Vm{
Test: &vm_pb.Vm_Test{
TestSuite: proto.String(testSuiteName),
TestRegex: proto.String(testRun),
},
Name: proto.String(name),
Id: proto.String(id),
Zone: proto.String(zone),
MachineType: proto.String(machineType),
}
vmInfo, err := proto.Marshal(vmInfoProto)
if err != nil {
log.Fatalf("failed to marshal vm info proto: %v", err)
}
if err = uploadGCSObject(ctx, client, propertiesURL, bytes.NewReader(vmInfo)); err != nil {
log.Fatalf("failed to upload vm info: %v", err)
}
}
func executeCmd(cmd, dir string, arg []string) ([]byte, error) {
command := exec.Command(cmd, arg...)
command.Dir = dir
log.Printf("Going to execute: %q", command.String())
output, err := command.Output()
if err != nil {
return output, err
}
return output, nil
}
func uploadGCSObject(ctx context.Context, client *storage.Client, path string, data io.Reader) error {
u, err := url.Parse(path)
if err != nil {
log.Fatalf("failed to parse gcs url: %v", err)
}
object := strings.TrimPrefix(u.Path, "/")
log.Printf("uploading to bucket %s object %s\n", u.Host, object)
upload := func() error {
dst := client.Bucket(u.Host).Object(object).NewWriter(ctx)
if _, err := io.Copy(dst, data); err != nil {
return fmt.Errorf("failed to write to gcs: %w", err)
}
if err := dst.Close(); err != nil {
return fmt.Errorf("failed to close gcs writer: %w", err)
}
return nil
}
var uploadErr error
for i := 1; i <= 5; i++ {
if uploadErr = upload(); uploadErr != nil {
time.Sleep(time.Duration(i) * time.Second)
continue
}
break
}
return uploadErr
}