Skip to content

Commit

Permalink
Add experiment support
Browse files Browse the repository at this point in the history
  • Loading branch information
Joshua Krstic committed Sep 6, 2023
1 parent d86a047 commit c8d59dc
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 0 deletions.
97 changes: 97 additions & 0 deletions internal/experiments/experiments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Package experiments contains functionalities to retrieve synced experiments
package experiments

import (
"encoding/json"
"io"
"os"
)

// GetBooleanExperiment attempts to read the key from the map provided and does type checking on the
// returned value, if any. Value is false on failure.
func GetBooleanExperiment(e map[string]any, key string) (bool, bool) {
v, err := e[key]
if !err {
return false, false
}

f, ok := v.(bool)
if !ok {
return false, false
}

return f, true
}

// GetStringExperiment attempts to read the key from the map provided and does type checking on the
// returned value, if any. Value is an empty string on failure.
func GetStringExperiment(e map[string]any, key string) (string, bool) {
v, err := e[key]
if !err {
return "", false
}

f, ok := v.(string)
if !ok {
return "", false
}

return f, true
}

// GetIntExperiment attempts to read the key from the map provided and does type checking on the
// returned value, if any. Value is -1 on failure.
// encoding/JSON doesn't differentiate between ints and floats. This method casts the float from
// the unmarshalled json to an int.
func GetIntExperiment(e map[string]any, key string) (int, bool) {
v, err := e[key]
if !err {
return -1, false
}

f, ok := v.(float64)
if !ok {
return -1, false
}

return int(f), true
}

// GetFloatExperiment attempts to read the key from the map provided and does type checking on the
// returned value, if any. Value is -1 on failure.
func GetFloatExperiment(e map[string]any, key string) (float64, bool) {
v, err := e[key]
if !err {
return -1, false
}

f, ok := v.(float64)
if !ok {
return -1, false
}

return f, true
}

// ReadExperimentsFile takes a filepath, opens the file, and calls ReadJsonInput with the contents
// of the file.
// If the file cannot be opened, the experiments map is set to an empty map.
func ReadExperimentsFile(fpath string) (map[string]any, error) {
f, err := os.Open(fpath)
if err != nil {
return map[string]any{}, err
}

return readJSONInput(f)
}

// ReadJSONInput takes a reader and unmarshalls the contents into the experiments map.
// If the unmarsahlling fails, the experiments map is set to an empty map.
func readJSONInput(read io.Reader) (map[string]any, error) {
jsondata := make(map[string]any)
if err := json.NewDecoder(read).Decode(&jsondata); err != nil {
return map[string]any{}, err
}

return jsondata, nil
}
251 changes: 251 additions & 0 deletions internal/experiments/experiments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package experiments

import (
"bytes"
"testing"
)

func TestGetBooleanExperiment(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"TestFeatureForImage\":true}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetBooleanExperiment(e, "TestFeatureForImage")
if !ok {
t.Error("failed to get key 'TestFeatureForImage'")
}
if val != true {
t.Errorf("expected 'TestFeatureForImage' to be true, got: %v", val)
}
}

func TestGetBooleanExperimentNoKey(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetBooleanExperiment(e, "InvalidKey")
if ok {
t.Errorf("expected to fail to get boolean experiment, got %v", val)
}
}

func TestGetBooleanExperimentInvalidType(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"StringFeature\":\"string_value\"}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetBooleanExperiment(e, "StringFeature")
if ok {
t.Errorf("expected to fail to get boolean experiment, got %v", val)
}
}

func TestGetStringExperiment(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"StringFeature\":\"string_value\"}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetStringExperiment(e, "StringFeature")
if !ok {
t.Error("failed to get key 'StringFeature'")
}
if val != "string_value" {
t.Errorf("expected 'StringFeature' to be 'string_value', got: %v", val)
}
}

func TestGetStringExperimentNoKey(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetBooleanExperiment(e, "InvalidKey")
if ok {
t.Errorf("expected to fail to get string experiment, got %v", val)
}
}

func TestGetStringExperimentInvalidType(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"FloatFeature\":-5.6}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetStringExperiment(e, "FloatFeature")
if ok {
t.Errorf("expected to fail to get string experiment, got %v", val)
}
}

func TestGetFloatExperiment(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"FloatFeature\":-5.6}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetFloatExperiment(e, "FloatFeature")
if !ok {
t.Error("failed to get key 'FloatFeature'")
}
if val != -5.6 {
t.Errorf("expected 'FloatFeature' to be -5.6, got: %v", val)
}
}

func TestGetFloatExperimentNoKey(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetFloatExperiment(e, "InvalidKey")
if ok {
t.Errorf("expected to fail to get string experiment, got %v", val)
}
}

func TestGetFloatExperimentInvalidType(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"TestFeatureForImage\":true}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetFloatExperiment(e, "TestFeatureForImage")
if ok {
t.Errorf("expected to fail to get float experiment, got %v", val)
}
}

func TestGetIntExperiment(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"IntFeature\":10293812}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetIntExperiment(e, "IntFeature")
if !ok {
t.Error("failed to get key 'IntFeature'")
}
if val != 10293812 {
t.Errorf("expected 'IntFeature' to be 10293812, got: %v", val)
}
}

func TestGetIntExperimentNoKey(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetIntExperiment(e, "InvalidKey")
if ok {
t.Errorf("expected to fail to get int experiment, got %v", val)
}
}

func TestGetIntExperimentInvalidType(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"StringFeature\":\"string_value\"}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetIntExperiment(e, "StringFeature")
if ok {
t.Errorf("expected to fail to get int experiment, got %v", val)
}
}

func TestGetSeveralExperiments(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"TestFeatureForImage\":true,\"StringFeature\":\"string_value\",\"FloatFeature\":-5.6,\"OtherTestFeatureForImage\":false}")

e, err := readJSONInput(&buffer)
if err != nil {
t.Errorf("Failed to parse input json, err: %v", err)
}

val, ok := GetBooleanExperiment(e, "TestFeatureForImage")
if !ok {
t.Error("failed to get key 'TestFeatureForImage'")
}
if val != true {
t.Errorf("expected 'TestFeatureForImage' to be true, got: %v", val)
}

val, ok = GetBooleanExperiment(e, "OtherTestFeatureForImage")
if !ok {
t.Error("failed to get key 'TestFeatureForImage'")
}
if val != false {
t.Errorf("expected 'OtherTestFeatureForImage' to be false, got: %v", val)
}
}

func TestReadExperimentsFileReturnsEmptyOnNoFile(t *testing.T) {
e, err := ReadExperimentsFile("$!bad path//")

if err == nil {
t.Errorf("expected err to be non nil, got nil")
}

if e == nil || len(e) > 0 {
t.Errorf("expected empty experiments object, got %v", e)
}
}

func TestReadExperimentsFileReturnsEmptyOnBadJsonFormat(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("{\"this string isnt json")

e, err := readJSONInput(&buffer)

if err == nil {
t.Errorf("expected err to be non nil, got nil")
}

if e == nil || len(e) > 0 {
t.Errorf("expected empty experiments object, got %v", e)
}
}
8 changes: 8 additions & 0 deletions launcher/container_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-tpm-tools/cel"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/internal/experiments"
"github.com/google/go-tpm-tools/launcher/agent"
"github.com/google/go-tpm-tools/launcher/spec"
"github.com/google/go-tpm-tools/launcher/verifier"
Expand Down Expand Up @@ -507,6 +508,13 @@ func (r *ContainerRunner) Run(ctx context.Context) error {
return fmt.Errorf("failed to fetch and write OIDC token: %v", err)
}

val, ok := experiments.GetBooleanExperiment(r.launchSpec.Experiments, "TestFeatureForImage")
if !ok {
r.logger.Println("failed to get TestFeatureForImage from launchspec")
} else {
r.logger.Printf("TestFeatureForImage returned %v\n", val)
}

var streamOpt cio.Opt
switch r.launchSpec.LogRedirect {
case spec.Nowhere:
Expand Down
7 changes: 7 additions & 0 deletions launcher/image/cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ steps:
- |
cd launcher/launcher
CGO_ENABLED=0 go build -o ../image/launcher
- name: 'gcr.io/cloud-builders/gcloud'
id: DownloadExpBinary
entrypoint: 'gcloud'
args: ['storage',
'cp',
'gs://confidential-space-images_third-party/confidential_space_experiments',
'./launcher/image/confidential_space_experiments']
- name: 'gcr.io/cos-cloud/cos-customizer'
args: ['start-image-build',
'-build-context=launcher/image',
Expand Down
1 change: 1 addition & 0 deletions launcher/image/container-runner.service
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Wants=network-online.target gcr-online.target containerd.service
After=network-online.target gcr-online.target containerd.service

[Service]
ExecStartPre=/usr/share/oem/confidential_space/sync_experiments.sh
ExecStart=/usr/share/oem/confidential_space/cs_container_launcher
ExecStopPost=/usr/share/oem/confidential_space/exit_script.sh
Restart=no
Expand Down
Loading

0 comments on commit c8d59dc

Please sign in to comment.