diff --git a/go.mod b/go.mod index ea0f3978..c0239ae1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/packethost/hegel go 1.13 require ( - github.com/docker/go-metrics v0.0.1 // indirect github.com/golang/protobuf v1.4.2 github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 @@ -12,6 +11,6 @@ require ( github.com/packethost/pkg v0.0.0-20190715213007-7c3a64b4b5e3 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.5.1 - github.com/tinkerbell/tink v0.0.0-20200724140154-850584d46c8d + github.com/tinkerbell/tink v0.0.0-20200807143153-f5831e4a5fb8 google.golang.org/grpc v1.29.1 ) diff --git a/go.sum b/go.sum index 6975b73c..f8adfac9 100644 --- a/go.sum +++ b/go.sum @@ -25,7 +25,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20191212201129-5f9f41018e9d/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -61,6 +60,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -68,6 +68,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -98,8 +100,6 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kdeng3849/tink v0.0.0-20200713034415-dd6d5ea8d040 h1:YTVq8u523XC/5Ru1+3uVcqzrZLcBm+KCyx1bATR90+w= -github.com/kdeng3849/tink v0.0.0-20200713034415-dd6d5ea8d040/go.mod h1:76mIaisvbix90uCd7nRXXNO198WiL+TeJihDr4BXRas= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -207,12 +207,8 @@ github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tinkerbell/tink v0.0.0-20200708073808-997394055483 h1:tvPuScVBrNizM2+i3qj2+ZDD8EePx/UHohJMY/XiP4A= -github.com/tinkerbell/tink v0.0.0-20200708073808-997394055483/go.mod h1:fVMM7v8aqMiptIq3DZWleonNo30JIBA4CX7gT0vDaiU= -github.com/tinkerbell/tink v0.0.0-20200710050004-a68bec0e8c1b h1:J8RDGhwHOoMgD7M+IumyyUpBameMygH5qUqfJMDUZLI= -github.com/tinkerbell/tink v0.0.0-20200710050004-a68bec0e8c1b/go.mod h1:fVMM7v8aqMiptIq3DZWleonNo30JIBA4CX7gT0vDaiU= -github.com/tinkerbell/tink v0.0.0-20200724140154-850584d46c8d h1:n8Z9XVLfObhWMLnCvx7kf1KtuigdnU/EULDMHgQ6ILI= -github.com/tinkerbell/tink v0.0.0-20200724140154-850584d46c8d/go.mod h1:LCMa/UyQYNA0Tf6E26+94gjpy5jA8k0IM1fNryFfGns= +github.com/tinkerbell/tink v0.0.0-20200807143153-f5831e4a5fb8 h1:DGRtLnmp8nHAjj335oxZSBMZwGKq3zTF4Ukhg1Dk2tY= +github.com/tinkerbell/tink v0.0.0-20200807143153-f5831e4a5fb8/go.mod h1:HwV/Tvp3emVQqdHFuVhWvX7rQIODBCSbnz4PIsSb9v4= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.uber.org/atomic v1.2.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -291,6 +287,8 @@ google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dT google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 h1:fiNLklpBwWK1mth30Hlwk+fcdBmIALlgF5iy77O37Ig= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -304,8 +302,12 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/grpc_server.go b/grpc_server.go index a7f00efe..a6438841 100644 --- a/grpc_server.go +++ b/grpc_server.go @@ -1,9 +1,9 @@ package main import ( + "bytes" "context" "encoding/json" - "errors" "io" "math" "net" @@ -18,6 +18,7 @@ import ( "github.com/packethost/cacher/protos/cacher" "github.com/packethost/hegel/grpc/hegel" "github.com/packethost/hegel/metrics" + "github.com/pkg/errors" tink "github.com/tinkerbell/tink/protos/hardware" "google.golang.org/grpc/codes" "google.golang.org/grpc/peer" @@ -177,7 +178,7 @@ func exportHardware(hw []byte) ([]byte, error) { } func filterMetadata(hw []byte, filter string) ([]byte, error) { - var result interface{} + var result bytes.Buffer query, err := gojq.Parse(filter) if err != nil { return nil, err @@ -193,19 +194,27 @@ func filterMetadata(hw []byte, filter string) ([]byte, error) { if !ok { break } - if err, ok := v.(error); ok { - return nil, err + + if v == nil { + continue } - result = v - } - if resultString, ok := result.(string); ok { // if already a string, don't marshal - return []byte(resultString), nil - } - if result != nil { // if nil, don't marshal (json.Marshal(nil) returns "null") - return json.Marshal(result) + switch vv := v.(type) { + case error: + return nil, errors.Wrap(vv, "error while filtering with gojq") + case string: + result.WriteString(vv) + default: + marshalled, err := json.Marshal(vv) + if err != nil { + return nil, errors.Wrap(err, "error marshalling jq result") + } + result.Write(marshalled) + } + result.WriteRune('\n') } - return nil, nil + + return bytes.TrimSuffix(result.Bytes(), []byte("\n")), nil } // UnmarshalJSON implements the json.Unmarshaler interface for custom unmarshalling of exportedHardwareCacher @@ -415,20 +424,21 @@ func getByIP(ctx context.Context, s *server, userIP string) ([]byte, error) { req := &tink.GetRequest{ Ip: userIP, } - resp, err := s.hardwareClient.ByIP(ctx, req) // use wrapper? + resp, err := s.hardwareClient.ByIP(ctx, req) if err != nil { return nil, err } - if resp == nil { - return nil, errors.New("could not find hardware") - } - hw, err = json.Marshal(util.HardwareWrapper{Hardware: resp.(*tink.Hardware)}) if err != nil { return nil, errors.New("could not marshal hardware") } + + if string(hw) == "{}" { + return nil, errors.New("could not find hardware") + } + default: req := &cacher.GetRequest{ IP: userIP, @@ -439,11 +449,10 @@ func getByIP(ctx context.Context, s *server, userIP string) ([]byte, error) { return nil, err } - if resp == nil { + hw = []byte(resp.(*cacher.Hardware).JSON) + if string(hw) == "" { return nil, errors.New("could not find hardware") } - - hw = []byte(resp.(*cacher.Hardware).JSON) } return hw, nil diff --git a/grpc_server_test.go b/grpc_server_test.go index 9c2a410b..327a8487 100644 --- a/grpc_server_test.go +++ b/grpc_server_test.go @@ -11,26 +11,29 @@ import ( ) func TestGetByIPCacher(t *testing.T) { - t.Log("DATA_MODEL_VERSION (empty to use cacher):", os.Getenv("DATA_MODEL_VERSION")) - for name, test := range cacherGrpcTests { t.Log(name) + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) + os.Unsetenv("DATA_MODEL_VERSION") + hegelTestServer := &server{ log: logger, hardwareClient: hardwareGetterMock{test.json}, } - ehw, err := getByIP(context.Background(), hegelTestServer, test.remote) // returns hardware data as []byte + ehw, err := getByIP(context.Background(), hegelTestServer, mockUserIP) // returns hardware data as []byte if err != nil { t.Fatal("unexpected error while getting hardware by ip:", err) } hw := exportedHardwareCacher{} err = json.Unmarshal(ehw, &hw) - if err != nil { - if err.Error() != test.error { - t.Fatalf("unexpected error while unmarshalling, want: %v, got: %v\n", test.error, err.Error()) + if test.error != "" { + if err == nil { + t.Fatalf("unmarshal should have returned error: %v", test.error) + } else if err.Error() != test.error { + t.Fatalf("unmarshal returned wrong error, want: %v, got: %v\n", err, test.error) } - continue } if hw.State != test.state { @@ -72,8 +75,9 @@ func TestGetByIPCacher(t *testing.T) { } func TestGetByIPTinkerbell(t *testing.T) { + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) os.Setenv("DATA_MODEL_VERSION", "1") - t.Log("DATA_MODEL_VERSION:", os.Getenv("DATA_MODEL_VERSION")) for name, test := range tinkerbellGrpcTests { t.Log(name) @@ -82,12 +86,13 @@ func TestGetByIPTinkerbell(t *testing.T) { log: logger, hardwareClient: hardwareGetterMock{test.json}, } - ehw, err := getByIP(context.Background(), hegelTestServer, test.remote) // returns hardware data as []byte - if err != nil { - if err.Error() != test.error { - t.Fatalf("unexpected error in getByIP, want: %v, got: %v\n", test.error, err.Error()) + ehw, err := getByIP(context.Background(), hegelTestServer, mockUserIP) // returns hardware data as []byte + if test.error != "" { + if err == nil { + t.Fatalf("getByIP should have returned error: %v", test.error) + } else if err.Error() != test.error { + t.Fatalf("getByIP returned wrong error: got %v want %v", err, test.error) } - continue } hw := struct { @@ -96,7 +101,7 @@ func TestGetByIPTinkerbell(t *testing.T) { }{} err = json.Unmarshal(ehw, &hw) if err != nil { - t.Error("Error in unmarshalling hardware metadata", err) + t.Error("error in unmarshalling hardware metadata", err) } if hw.ID != test.id { @@ -142,9 +147,29 @@ func TestGetByIPTinkerbell(t *testing.T) { } } +func TestFilterMetadata(t *testing.T) { + for name, test := range tinkerbellFilterMetadataTests { + t.Run(name, func(t *testing.T) { + + res, err := filterMetadata([]byte(test.json), test.filter) + if test.error != "" { + if err == nil { + t.Errorf("filterMetadata should have returned error: %v", test.error) + } else if err.Error() != test.error { + t.Errorf("filterMetadata returned wrong error: got %v want %v", err, test.error) + } + } + + if string(res) != test.result { + t.Errorf("filterMetadata returned wrong result: got %s want %v", res, test.result) + } + }) + } +} + +// test cases for TestGetByIPCacher var cacherGrpcTests = map[string]struct { id string - remote string state string facility string mac string @@ -159,7 +184,6 @@ var cacherGrpcTests = map[string]struct { error string }{ "cacher": { - remote: "192.168.1.5", state: "provisioning", facility: "onprem", mac: "98:03:9b:48:de:bc", @@ -249,9 +273,9 @@ var cacherGrpcTests = map[string]struct { }, } +// test cases for TestGetByIPTinkerbell var tinkerbellGrpcTests = map[string]struct { id string - remote string state string bondingMode int64 diskDevice string @@ -266,7 +290,6 @@ var tinkerbellGrpcTests = map[string]struct { }{ "tinkerbell": { id: "fde7c87c-d154-447e-9fce-7eb7bdec90c0", - remote: "192.168.1.5", bondingMode: 5, diskDevice: "/dev/sda", wipeTable: true, @@ -278,8 +301,82 @@ var tinkerbellGrpcTests = map[string]struct { json: tinkerbellDataModel, }, "tinkerbell no metadata": { - id: "363115b0-f03d-4ce5-9a15-5514193d131a", - remote: "192.168.1.5", - json: tinkerbellNoMetadata, + id: "363115b0-f03d-4ce5-9a15-5514193d131a", + json: tinkerbellNoMetadata, + }, +} + +// test cases for TestFilterMetadata +var tinkerbellFilterMetadataTests = map[string]struct { + filter string + result string + error string + json string +}{ + "single result (simple)": { + filter: ec2Filters["/user-data"], + result: `#!/bin/bash + +echo "Hello world!"`, + json: tinkerbellFilterMetadata, + }, + "single result (complex)": { + filter: ec2Filters["/meta-data/public-ipv4"], + result: "139.175.86.114", + json: tinkerbellFilterMetadata, + }, + "multiple results (separated list results from hardware)": { + filter: ec2Filters["/meta-data/tags"], + result: `hello +test`, + json: tinkerbellFilterMetadata, + }, + "multiple results (separated list results from filter)": { + filter: ec2Filters["/meta-data/operating-system"], + result: `distro +image_tag +license_activation +slug +version`, + json: tinkerbellFilterMetadata, + }, + "multiple results (/meta-data filter with spot field present)": { + filter: ec2Filters["/meta-data"], + result: `facility +hostname +instance-id +iqn +local-ipv4 +operating-system +plan +public-ipv4 +public-ipv6 +public-keys +spot +tags`, + json: tinkerbellFilterMetadata, + }, + "invalid filter syntax": { + filter: "invalid", + error: "error while filtering with gojq: function not defined: invalid/0", + json: tinkerbellFilterMetadata, + }, + "valid filter syntax, nonexistent field": { + filter: "metadata.nonexistent", + json: tinkerbellFilterMetadata, + }, + "empty string filter": { + filter: "", + result: tinkerbellFilterMetadata, + json: tinkerbellFilterMetadata, + }, + "list filter on nonexistent field, without '?'": { + filter: ".metadata.nonexistent[]", + error: "error while filtering with gojq: cannot iterate over: null", + json: tinkerbellFilterMetadata, + }, + "list filter on nonexistent field, with '?'": { + filter: ".metadata.nonexistent[]?", + json: tinkerbellFilterMetadata, }, } diff --git a/http_handlers.go b/http_handlers.go index 3396f792..75bef38a 100644 --- a/http_handlers.go +++ b/http_handlers.go @@ -14,6 +14,37 @@ import ( "github.com/packethost/hegel/metrics" ) +var ( + // ec2Filters defines the query pattern and filters for the EC2 endpoint + // for queries that are to return another list of metadata items, the filter is a static list of the metadata items ("directory-listing filter") + // for /meta-data, the `spot` metadata item will only show up when the instance is a spot instance (denoted by if the `spot` field inside hardware is nonnull) + // NOTE: make sure when adding a new metadata item in a "subdirectory", to also add it to the directory-listing filter + ec2Filters = map[string]string{ + "": `"meta-data", "user-data"`, // base path + "/user-data": ".metadata.userdata", + "/meta-data": `["instance-id", "hostname", "iqn", "plan", "facility", "tags", "operating-system", "public-keys", "public-ipv4", "public-ipv6", "local-ipv4"] + (if .metadata.instance.spot != null then ["spot"] else [] end) | sort | .[]`, + "/meta-data/instance-id": ".metadata.instance.id", + "/meta-data/hostname": ".metadata.instance.hostname", + "/meta-data/iqn": ".metadata.instance.iqn", + "/meta-data/plan": ".metadata.instance.plan", + "/meta-data/facility": ".metadata.instance.facility", + "/meta-data/tags": ".metadata.instance.tags[]?", + "/meta-data/operating-system": `["slug", "distro", "version", "license_activation", "image_tag"] | sort | .[]`, + "/meta-data/operating-system/slug": ".metadata.instance.operating_system.slug", + "/meta-data/operating-system/distro": ".metadata.instance.operating_system.distro", + "/meta-data/operating-system/version": ".metadata.instance.operating_system.version", + "/meta-data/operating-system/license_activation": `"state"`, + "/meta-data/operating-system/license_activation/state": ".metadata.instance.operating_system.license_activation.state", + "/meta-data/operating-system/image_tag": ".metadata.instance.operating_system.image_tag", + "/meta-data/public-keys": ".metadata.instance.ssh_keys[]?", + "/meta-data/spot": `"termination-time"`, + "/meta-data/spot/termination-time": ".metadata.instance.spot.termination_time", + "/meta-data/public-ipv4": ".metadata.instance.network.addresses[]? | select(.address_family == 4 and .public == true) | .address", + "/meta-data/public-ipv6": ".metadata.instance.network.addresses[]? | select(.address_family == 6 and .public == true) | .address", + "/meta-data/local-ipv4": ".metadata.instance.network.addresses[]? | select(.address_family == 4 and .public == false) | .address", + } +) + func versionHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := w.Write(gitRevJSON) @@ -57,22 +88,24 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) { func getMetadata(filter string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) return } - logger.Debug("Calling getMetadata ") + logger.Debug("calling getMetadata ") userIP := getIPFromRequest(r) if userIP == "" { return } metrics.MetadataRequests.Inc() - logger.With("userIP", userIP).Info("Actual IP is: ") + l := logger.With("userIP", userIP) + l.Info("got ip from request") hw, err := getByIP(context.Background(), hegelServer, userIP) // returns hardware data as []byte if err != nil { metrics.Errors.WithLabelValues("metadata", "lookup").Inc() - logger.Info("Error in finding hardware: ", err) - w.WriteHeader(http.StatusInternalServerError) + l.With("error", err).Info("failed to get hardware by ip") + w.WriteHeader(http.StatusNotFound) return } @@ -82,26 +115,93 @@ func getMetadata(filter string) http.HandlerFunc { case "": resp, err = exportHardware(hw) // in cacher mode, the "filter" is the exportedHardwareCacher type if err != nil { - logger.Info("Error in exporting hardware: ", err) + l.With("error", err).Info("failed to export hardware") + w.WriteHeader(http.StatusInternalServerError) + return } case "1": resp, err = filterMetadata(hw, filter) if err != nil { - logger.Info("Error in filtering metadata: ", err) + l.With("error", err).Info("failed to filter metadata") + w.WriteHeader(http.StatusInternalServerError) + return } default: - logger.Fatal(errors.New("unknown DATA_MODEL_VERSION")) + l.Fatal(errors.New("unknown DATA_MODEL_VERSION")) + w.WriteHeader(http.StatusInternalServerError) + return } w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") _, err = w.Write(resp) if err != nil { - logger.Error(err, "failed to write Metadata") + l.With("error", err).Info("failed to write response") } } } +func ec2Handler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + logger.Debug("calling ec2Handler ") + userIP := getIPFromRequest(r) + if userIP == "" { + return + } + + metrics.MetadataRequests.Inc() + l := logger.With("userIP", userIP) + l.Info("got ip from request") + hw, err := getByIP(context.Background(), hegelServer, userIP) // returns hardware data as []byte + if err != nil { + metrics.Errors.WithLabelValues("metadata", "lookup").Inc() + l.With("error", err).Info("failed to get hardware by ip") + w.WriteHeader(http.StatusNotFound) + return + } + + filter, err := processEC2Query(r.URL.Path) + if err != nil { + l.With("error", err).Info("failed to process ec2 query") + w.WriteHeader(http.StatusNotFound) + _, err := w.Write([]byte("404 not found")) + if err != nil { + l.With("error", err).Info("failed to write response") + } + return + } + + var resp []byte + resp, err = filterMetadata(hw, filter) + if err != nil { + l.With("error", err).Info("failed to filter metadata") + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(resp) + if err != nil { + l.With("error", err).Info("failed to write response") + } +} + +// processEC2Query returns either a specific filter (used to parse hardware data for the value of a specific field), +// or a comma-separated list of metadata items (to be printed) +func processEC2Query(url string) (string, error) { + query := strings.TrimRight(strings.TrimPrefix(url, "/2009-04-04"), "/") // remove base pattern and trailing slash + + filter, ok := ec2Filters[query] + if !ok { + return "", errors.New("invalid metadata item") + } + + return filter, nil +} + func getIPFromRequest(r *http.Request) string { IPAddress := r.RemoteAddr if strings.ContainsRune(IPAddress, ':') { diff --git a/http_handlers_test.go b/http_handlers_test.go index dfb17fe8..9e5190d8 100644 --- a/http_handlers_test.go +++ b/http_handlers_test.go @@ -5,6 +5,10 @@ import ( "net/http" "net/http/httptest" "os" + "path" + "reflect" + "sort" + "strings" "testing" "github.com/tinkerbell/tink/protos/packet" @@ -15,14 +19,19 @@ func TestGetMetadataCacher(t *testing.T) { t.Log(name) hegelServer.hardwareClient = hardwareGetterMock{test.json} - os.Setenv("DATA_MODEL_VERSION", "") + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) + os.Unsetenv("DATA_MODEL_VERSION") + + customEndpoints := os.Getenv("CUSTOM_ENDPOINTS") + defer os.Setenv("CUSTOM_ENDPOINTS", customEndpoints) os.Unsetenv("CUSTOM_ENDPOINTS") req, err := http.NewRequest("GET", "/metadata", nil) if err != nil { t.Fatal(err) } - req.RemoteAddr = test.remote + req.RemoteAddr = mockUserIP resp := httptest.NewRecorder() http.HandleFunc("/metadata", getMetadata("")) // filter not used in cacher mode @@ -52,7 +61,12 @@ func TestGetMetadataCacher(t *testing.T) { // TestGetMetadataTinkerbell tests the default use case in tinkerbell mode func TestGetMetadataTinkerbell(t *testing.T) { + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) os.Setenv("DATA_MODEL_VERSION", "1") + + customEndpoints := os.Getenv("CUSTOM_ENDPOINTS") + defer os.Setenv("CUSTOM_ENDPOINTS", customEndpoints) os.Unsetenv("CUSTOM_ENDPOINTS") for name, test := range tinkerbellTests { @@ -60,6 +74,7 @@ func TestGetMetadataTinkerbell(t *testing.T) { hegelServer.hardwareClient = hardwareGetterMock{test.json} http.DefaultServeMux = &http.ServeMux{} // reset registered patterns + err := registerCustomEndpoints() if err != nil { t.Fatal("Error registering custom endpoints", err) @@ -69,7 +84,7 @@ func TestGetMetadataTinkerbell(t *testing.T) { if err != nil { t.Fatal(err) } - req.RemoteAddr = test.remote + req.RemoteAddr = mockUserIP resp := httptest.NewRecorder() http.DefaultServeMux.ServeHTTP(resp, req) @@ -98,7 +113,12 @@ func TestGetMetadataTinkerbell(t *testing.T) { // TestGetMetadataTinkerbellKant tests the kant specific use case in tinkerbell mode func TestGetMetadataTinkerbellKant(t *testing.T) { + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) os.Setenv("DATA_MODEL_VERSION", "1") + + customEndpoints := os.Getenv("CUSTOM_ENDPOINTS") + defer os.Setenv("CUSTOM_ENDPOINTS", customEndpoints) os.Setenv("CUSTOM_ENDPOINTS", `{"/metadata":".metadata.instance","/components":".metadata.components","/userdata":".metadata.userdata"}`) for name, test := range tinkerbellKantTests { @@ -106,6 +126,7 @@ func TestGetMetadataTinkerbellKant(t *testing.T) { hegelServer.hardwareClient = hardwareGetterMock{test.json} http.DefaultServeMux = &http.ServeMux{} // reset registered patterns + err := registerCustomEndpoints() if err != nil { t.Fatal("Error registering custom endpoints", err) @@ -115,7 +136,7 @@ func TestGetMetadataTinkerbellKant(t *testing.T) { if err != nil { t.Fatal(err) } - req.RemoteAddr = test.remote + req.RemoteAddr = mockUserIP resp := httptest.NewRecorder() http.DefaultServeMux.ServeHTTP(resp, req) @@ -133,8 +154,13 @@ func TestGetMetadataTinkerbellKant(t *testing.T) { } func TestRegisterEndpoints(t *testing.T) { + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) os.Setenv("DATA_MODEL_VERSION", "1") + customEndpoints := os.Getenv("CUSTOM_ENDPOINTS") + defer os.Setenv("CUSTOM_ENDPOINTS", customEndpoints) + for name, test := range registerEndpointTests { t.Log(name) hegelServer.hardwareClient = hardwareGetterMock{test.json} @@ -145,16 +171,21 @@ func TestRegisterEndpoints(t *testing.T) { } http.DefaultServeMux = &http.ServeMux{} // reset registered patterns + err := registerCustomEndpoints() - if err != nil { - t.Fatal("Error registering custom endpoints", err) + if test.error != "" { + if err == nil { + t.Fatalf("registerCustomEndpoints should have returned error: %v", test.error) + } else if err.Error() != test.error { + t.Fatalf("registerCustomEndpoints returned wrong error: got %v want %v", err, test.error) + } } req, err := http.NewRequest("GET", test.url, nil) if err != nil { t.Fatal(err) } - req.RemoteAddr = test.remote + req.RemoteAddr = mockUserIP resp := httptest.NewRecorder() http.DefaultServeMux.ServeHTTP(resp, req) @@ -171,63 +202,148 @@ func TestRegisterEndpoints(t *testing.T) { } } +func TestEC2Endpoint(t *testing.T) { + dataModelVersion := os.Getenv("DATA_MODEL_VERSION") + defer os.Setenv("DATA_MODEL_VERSION", dataModelVersion) + os.Setenv("DATA_MODEL_VERSION", "1") + + for name, test := range tinkerbellEC2Tests { + t.Run(name, func(t *testing.T) { + hegelServer.hardwareClient = hardwareGetterMock{test.json} + + http.DefaultServeMux = &http.ServeMux{} // reset registered patterns + + http.HandleFunc("/2009-04-04", ec2Handler) // workaround for making trailing slash optional + http.HandleFunc("/2009-04-04/", ec2Handler) + + req, err := http.NewRequest("GET", test.url, nil) + if err != nil { + t.Fatal(err) + } + req.RemoteAddr = mockUserIP + resp := httptest.NewRecorder() + + http.DefaultServeMux.ServeHTTP(resp, req) + + if status := resp.Code; status != test.status { + t.Errorf("handler returned wrong status code: got %v want %v", + status, test.status) + } + + if resp.Body.String() != test.response { + t.Errorf("handler returned wrong body: got %v want %v", resp.Body.String(), test.response) + } + }) + } +} + +func TestProcessEC2Query(t *testing.T) { + for name, test := range processEC2QueryTests { + t.Run(name, func(t *testing.T) { + + res, err := processEC2Query(test.url) + if test.error != "" { + if err == nil { + t.Fatalf("processEC2Query should have returned error: %v", test.error) + } else if err.Error() != test.error { + t.Fatalf("processEC2Query returned wrong error: got %v want %v", err, test.error) + } + } + + if !reflect.DeepEqual(res, test.result) { + t.Errorf("handler returned wrong result: got %v want %v", res, test.result) + } + }) + } +} + +// TestEC2FiltersMap checks if the all the metadata items are listed in their corresponding directory-listing filter +// itemsFromQueries are the metadata items "extracted" from the queries (keys) of the ec2Filters map +// itemsFromFilter are the metadata items "extracted" from the filters (values) of the ec2Filters map +func TestEC2FiltersMap(t *testing.T) { + directories := make(map[string][]string) // keys are the directory base paths; values are a list of metadata items that are under the base paths + + for query := range ec2Filters { + basePath, metadataItem := path.Split(query) + directories[basePath] = append(directories[basePath], metadataItem) + } + + for basePath, metadataItems := range directories { + if basePath == "" { // ignore the `"": []` entry + continue + } + t.Log("base path:", basePath) + + hw := `{"metadata":{"instance":{"spot":{}}}}` // to make sure the 'spot' metadata item will be included + query := strings.TrimSuffix(basePath, "/") + dirListFilter := ec2Filters[query] // get the directory-list filter + itemsFromFilter, err := filterMetadata([]byte(hw), dirListFilter) + if err != nil { + t.Errorf("failed to filter metadata: %s", err) + } + + sort.Strings(metadataItems) + itemsFromQueries := strings.Join(metadataItems, "\n") + + if string(itemsFromFilter) != itemsFromQueries { + t.Error("directory-list does not match the actual queries") + t.Errorf("from filter: %s", itemsFromFilter) + t.Errorf("from queries: %s", itemsFromQueries) + } + } +} + +// test cases for TestGetMetadataCacher var cacherTests = map[string]struct { id string - remote string planSlug string json string }{ "cacher": { id: "8978e7d4-1a55-4845-8a66-a5259236b104", - remote: "192.168.1.5", planSlug: "t1.small.x86", json: cacherDataModel, }, } +// test cases for TestGetMetadataTinkerbell var tinkerbellTests = map[string]struct { id string - remote string bondingMode int64 json string }{ "tinkerbell": { id: "fde7c87c-d154-447e-9fce-7eb7bdec90c0", - remote: "192.168.1.5", bondingMode: 5, json: tinkerbellDataModel, }, "tinkerbell no metadata": { - id: "363115b0-f03d-4ce5-9a15-5514193d131a", - remote: "192.168.1.5", - json: tinkerbellNoMetadata, + id: "363115b0-f03d-4ce5-9a15-5514193d131a", + json: tinkerbellNoMetadata, }, } +// test cases for TestGetMetadataTinkerbellKant var tinkerbellKantTests = map[string]struct { url string - remote string status int response string json string }{ "metadata endpoint": { url: "/metadata", - remote: "192.168.1.5", status: 200, response: `{"facility":"sjc1","hostname":"tink-provisioner","id":"f955e31a-cab6-44d6-872c-9614c2024bb4"}`, json: tinkerbellKant, }, "components endpoint": { url: "/components", - remote: "192.168.1.5", status: 200, response: `{"id":"bc9ce39b-7f18-425b-bc7b-067914fa9786","type":"DiskComponent"}`, json: tinkerbellKant, }, "userdata endpoint": { url: "/userdata", - remote: "192.168.1.5", status: 200, response: `#!/bin/bash @@ -236,68 +352,236 @@ echo "Hello world!"`, }, "no metadata": { url: "/metadata", - remote: "192.168.1.5", status: 200, response: "", json: tinkerbellNoMetadata, }, } +// test cases for TestRegisterEndpoints var registerEndpointTests = map[string]struct { customEndpoints string url string - remote string status int expectResponseEmpty bool + error string json string }{ "single custom endpoint": { customEndpoints: `{"/facility": ".metadata.facility"}`, url: "/facility", - remote: "192.168.1.5", + status: 200, + json: tinkerbellDataModel, + }, + "single custom endpoint, non-metadata": { + customEndpoints: `{"/id": ".id"}`, + url: "/id", status: 200, json: tinkerbellDataModel, }, "single custom endpoint, invalid url call": { customEndpoints: `{"/userdata": ".metadata.userdata"}`, url: "/metadata", - remote: "192.168.1.5", status: 404, json: tinkerbellDataModel, }, "multiple custom endpoints": { customEndpoints: `{"/metadata":".metadata.instance","/components":".metadata.components","/all":".","/userdata":".metadata.userdata"}`, url: "/components", - remote: "192.168.1.5", status: 200, json: tinkerbellDataModel, }, "default endpoint": { url: "/metadata", - remote: "192.168.1.5", status: 200, json: tinkerbellDataModel, }, "custom endpoints invalid format (not a map)": { customEndpoints: `"/userdata":".metadata.userdata"`, url: "/userdata", - remote: "192.168.1.5", status: 404, + error: "error in parsing custom endpoints: invalid character ':' after top-level value", json: tinkerbellDataModel, }, "custom endpoints invalid format (endpoint missing forward slash)": { customEndpoints: `{"userdata":".metadata.userdata"}`, url: "/userdata", - remote: "192.168.1.5", status: 404, json: tinkerbellDataModel, }, - "custom endpoints invalid format (invalid jq filter)": { + "custom endpoints invalid format (invalid jq filter syntax)": { customEndpoints: `{"/userdata":"invalid"}`, url: "/userdata", - remote: "192.168.1.5", + status: 500, + expectResponseEmpty: true, + json: tinkerbellDataModel, + }, + "custom endpoints invalid format (valid jq filter syntax, nonexistent field)": { + customEndpoints: `{"/userdata":".nonexistent"}`, + url: "/userdata", status: 200, expectResponseEmpty: true, json: tinkerbellDataModel, }, } + +// test cases for TestEC2Endpoint +var tinkerbellEC2Tests = map[string]struct { + url string + status int + response string + json string +}{ + "user-data": { + url: "/2009-04-04/user-data", + status: 200, + response: `#!/bin/bash + +echo "Hello world!"`, + json: tinkerbellKantEC2, + }, + "meta-data": { + url: "/2009-04-04/meta-data", + status: 200, + response: `facility +hostname +instance-id +iqn +local-ipv4 +operating-system +plan +public-ipv4 +public-ipv6 +public-keys +tags`, + json: tinkerbellKantEC2, + }, + "instance-id": { + url: "/2009-04-04/meta-data/instance-id", + status: 200, + response: "7c9a5711-aadd-4fa0-8e57-789431626a27", + json: tinkerbellKantEC2, + }, + "public-ipv4": { + url: "/2009-04-04/meta-data/public-ipv4", + status: 200, + response: "139.175.86.114", + json: tinkerbellKantEC2, + }, + "public-ipv6": { + url: "/2009-04-04/meta-data/public-ipv6", + status: 200, + response: "2604:1380:1000:ca00::7", + json: tinkerbellKantEC2, + }, + "local-ipv4": { + url: "/2009-04-04/meta-data/local-ipv4", + status: 200, + response: "10.87.63.3", + json: tinkerbellKantEC2, + }, + "tags": { + url: "/2009-04-04/meta-data/tags", + status: 200, + response: `hello +test`, + json: tinkerbellKantEC2, + }, + "operating-system slug": { + url: "/2009-04-04/meta-data/operating-system/slug", + status: 200, + response: "ubuntu_18_04", + json: tinkerbellKantEC2, + }, + "invalid metadata item": { + url: "/2009-04-04/meta-data/invalid", + status: 404, + response: "404 not found", + json: tinkerbellKantEC2, + }, + "valid metadata item, but not found": { + url: "/2009-04-04/meta-data/public-keys", + status: 200, + response: "", + json: tinkerbellNoMetadata, + }, + "with trailing slash": { + url: "/2009-04-04/meta-data/hostname/", + status: 200, + response: "tink-provisioner", + json: tinkerbellKantEC2, + }, + "base endpoint": { + url: "/2009-04-04", + status: 200, + response: `meta-data +user-data`, + json: tinkerbellKantEC2, + }, + "base endpoint with trailing slash": { + url: "/2009-04-04/", + status: 200, + response: `meta-data +user-data`, + json: tinkerbellKantEC2, + }, + "spot instance with empty (but still present) spot field": { + url: "/2009-04-04/meta-data", + status: 200, + response: `facility +hostname +instance-id +iqn +local-ipv4 +operating-system +plan +public-ipv4 +public-ipv6 +public-keys +spot +tags`, + json: tinkerbellKantEC2SpotEmpty, + }, + "termination-time": { + url: "/2009-04-04/meta-data/spot/termination-time", + status: 200, + response: "now", + json: tinkerbellKantEC2SpotWithTermination, + }, +} + +// test cases for TestProcessEC2Query +var processEC2QueryTests = map[string]struct { + url string + error string + result string +}{ + "hardware filter result (simple query)": { + url: "/2009-04-04/user-data", + result: ec2Filters["/user-data"], + }, + "hardware filter result (long query)": { + url: "/2009-04-04/meta-data/operating-system/license_activation/state", + result: ec2Filters["/meta-data/operating-system/license_activation/state"], + }, + "directory-listing filter result": { + url: "/2009-04-04/meta-data/operating-system/license_activation", + result: ec2Filters["/meta-data/operating-system/license_activation"], + }, + "directory-listing filter result (base endpoint)": { + url: "/2009-04-04/", + result: ec2Filters[""], + }, + "directory-listing result (/meta-data endpoint)": { + url: "/2009-04-04/meta-data", + result: ec2Filters["/meta-data"], + }, + "invalid query (invalid metadata item)": { + url: "/2009-04-04/invalid", + error: "invalid metadata item", + }, + "invalid query (not a subdirectory)": { + url: "/2009-04-04/user-data/hostname", + error: "invalid metadata item", + }, +} diff --git a/main.go b/main.go index 69035a91..bef7ae7a 100644 --- a/main.go +++ b/main.go @@ -252,6 +252,9 @@ func main() { http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/_packet/healthcheck", healthCheckHandler) http.HandleFunc("/_packet/version", versionHandler) + http.HandleFunc("/2009-04-04", ec2Handler) // workaround for making trailing slash optional + http.HandleFunc("/2009-04-04/", ec2Handler) + err = registerCustomEndpoints() if err != nil { logger.Fatal(err, "could not register custom endpoints") @@ -281,7 +284,7 @@ func registerCustomEndpoints() error { endpoints := make(map[string]string) err := json.Unmarshal([]byte(customEndpoints), &endpoints) if err != nil { - logger.Info("Error in parsing custom endpoints: ", err) + return errors.Wrap(err, "error in parsing custom endpoints") } for endpoint, filter := range endpoints { http.HandleFunc(endpoint, getMetadata(filter)) diff --git a/mock.go b/mock.go index b15b48bb..0150f46e 100644 --- a/mock.go +++ b/mock.go @@ -11,7 +11,8 @@ import ( "google.golang.org/grpc" ) -// hardwareGetterMock is a mock implentation of the +// hardwareGetterMock is a mock implentation of the hardwareGetter interface +// hardwareResp represents the hardware data stored inside tink db type hardwareGetterMock struct { hardwareResp string } @@ -39,6 +40,7 @@ func (hg hardwareGetterMock) Watch(ctx context.Context, in getRequest, opts ...g } const ( + mockUserIP = "192.168.1.5" // value is completely arbitrary, as long as it's an IP to be parsed by getIPFromRequest (could even be 0.0.0.0) cacherDataModel = ` { "id": "8978e7d4-1a55-4845-8a66-a5259236b104", @@ -589,4 +591,81 @@ const ( "metadata": "{\"components\":{\"id\":\"bc9ce39b-7f18-425b-bc7b-067914fa9786\",\"type\":\"DiskComponent\"},\"instance\":{\"facility\":\"sjc1\",\"hostname\":\"tink-provisioner\",\"id\":\"f955e31a-cab6-44d6-872c-9614c2024bb4\"},\"userdata\":\"#!/bin/bash\\n\\necho \\\"Hello world!\\\"\"}" } ` + tinkerbellKantEC2 = ` +{ + "id":"0eba0bf8-3772-4b4a-ab9f-6ebe93b90a94", + "network":{ + "interfaces":[ + { + "dhcp":{ + "ip":{ + "address":"192.168.1.5", + "gateway":"192.168.1.1", + "netmask":"255.255.255.248" + }, + "mac":"b4:96:91:5f:af:c0", + "arch":"x86_64" + }, + "netboot":{ + "allow_pxe":true, + "allow_workflow":true + } + } + ] + }, + "metadata":"{\"components\":{\"id\":\"bc9ce39b-7f18-425b-bc7b-067914fa9786\",\"type\":\"DiskComponent\"},\"instance\":{\"api_url\":\"https://metadata.packet.net\",\"class\":\"c3.small.x86\",\"customdata\":{},\"facility\":\"sjc1\",\"hostname\":\"tink-provisioner\",\"id\":\"7c9a5711-aadd-4fa0-8e57-789431626a27\",\"iqn\":\"iqn.2020-06.net.packet:device.7c9a5711\",\"network\":{\"addresses\":[{\"address\":\"139.175.86.114\",\"address_family\":4,\"cidr\":31,\"created_at\":\"2020-06-19T04:16:08Z\",\"enabled\":true,\"gateway\":\"139.175.86.113\",\"id\":\"99e15f8e-6eab-40db-9c6f-69a69ef9854f\",\"management\":true,\"netmask\":\"255.255.255.254\",\"network\":\"139.175.86.113\",\"parent_block\":{\"cidr\":31,\"href\":\"/ips/179580b0-3ae4-4fc0-8cbe-4f34174bebb4\",\"netmask\":\"255.255.255.254\",\"network\":\"139.175.86.113\"},\"public\":true},{\"address\":\"2604:1380:1000:ca00::7\",\"address_family\":6,\"cidr\":127,\"created_at\":\"2020-06-19T04:16:08Z\",\"enabled\":true,\"gateway\":\"2604:1380:1000:ca00::6\",\"id\":\"f4b24331-c6cf-4ae4-899b-e78f223b2c57\",\"management\":true,\"netmask\":\"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe\",\"network\":\"2604:1380:1000:ca00::6\",\"parent_block\":{\"cidr\":56,\"href\":\"/ips/960aa63d-eeb6-410e-8242-1d6e2e7733fc\",\"netmask\":\"ffff:ffff:ffff:ff00:0000:0000:0000:0000\",\"network\":\"2604:1380:1000:ca00:0000:0000:0000:0000\"},\"public\":true},{\"address\":\"10.87.63.3\",\"address_family\":4,\"cidr\":31,\"created_at\":\"2020-06-19T04:16:08Z\",\"enabled\":true,\"gateway\":\"10.87.63.2\",\"id\":\"5cca13a9-43d0-45a6-9ed7-3d9e2fbf0e87\",\"management\":true,\"netmask\":\"255.255.255.254\",\"network\":\"10.87.63.2\",\"parent_block\":{\"cidr\":25,\"href\":\"/ips/7cde0a1b-d787-4a10-9c96-4049c7d5eeb3\",\"netmask\":\"255.255.255.128\",\"network\":\"10.87.63.0\"},\"public\":false}],\"bonding\":{\"link_aggregation\":null,\"mac\":\"b4:96:91:5f:ad:d8\",\"mode\":4},\"interfaces\":[{\"bond\":\"bond0\",\"mac\":\"b4:96:91:5f:ad:d8\",\"name\":\"eth0\"},{\"bond\":\"bond0\",\"mac\":\"b4:96:91:5f:ad:d9\",\"name\":\"eth1\"}]},\"operating_system\":{\"distro\":\"ubuntu\",\"image_tag\":\"f8f0331d31935319dfa8b6d551b5680840d7944f\",\"license_activation\":{\"state\":\"unlicensed\"},\"slug\":\"ubuntu_18_04\",\"version\":\"18.04\"},\"phone_home_url\":\"http://tinkerbell.sjc1.packet.net/phone-home\",\"plan\":\"c3.small.x86\",\"private_subnets\":[\"10.0.0.0/8\"],\"specs\":{\"cpus\":[{\"count\":1,\"type\":\"EPYC 3151 4 Core Processor @ 2.7GHz\"}],\"drives\":[{\"category\":\"boot\",\"count\":2,\"size\":\"240GB\",\"type\":\"SSD\"}],\"features\":{},\"memory\":{\"total\":\"16GB\"},\"nics\":[{\"count\":2,\"type\":\"10Gbps\"}]},\"ssh_keys\":[],\"storage\":{\"disks\":[{\"device\":\"/dev/sda\",\"partitions\":[{\"label\":\"BIOS\",\"number\":1,\"size\":4096},{\"label\":\"SWAP\",\"number\":2,\"size\":\"3993600\"},{\"label\":\"ROOT\",\"number\":3,\"size\":0}],\"wipeTable\":true}],\"filesystems\":[{\"mount\":{\"create\":{\"options\":[\"-L\",\"ROOT\"]},\"device\":\"/dev/sda3\",\"format\":\"ext4\",\"point\":\"/\"}},{\"mount\":{\"create\":{\"options\":[\"-L\",\"SWAP\"]},\"device\":\"/dev/sda2\",\"format\":\"swap\",\"point\":\"none\"}}]},\"switch_short_id\":\"68c7fa13\",\"tags\":[\"hello\",\"test\"],\"user_state_url\":\"http://tinkerbell.sjc1.packet.net/events\",\"volumes\":[]},\"userdata\":\"#!/bin/bash\\n\\necho \\\"Hello world!\\\"\"}" +} +` + tinkerbellKantEC2SpotEmpty = ` +{ + "id":"0eba0bf8-3772-4b4a-ab9f-6ebe93b90a94", + "network":{ + "interfaces":[ + { + "dhcp":{ + "ip":{ + "address":"192.168.1.5", + "gateway":"192.168.1.1", + "netmask":"255.255.255.248" + }, + "mac":"b4:96:91:5f:af:c0", + "arch":"x86_64" + }, + "netboot":{ + "allow_pxe":true, + "allow_workflow":true + } + } + ] + }, + "metadata":"{\"instance\":{\"spot\":{}}}" +} +` + tinkerbellKantEC2SpotWithTermination = ` +{ + "id":"0eba0bf8-3772-4b4a-ab9f-6ebe93b90a94", + "network":{ + "interfaces":[ + { + "dhcp":{ + "ip":{ + "address":"192.168.1.5", + "gateway":"192.168.1.1", + "netmask":"255.255.255.248" + }, + "mac":"b4:96:91:5f:af:c0", + "arch":"x86_64" + }, + "netboot":{ + "allow_pxe":true, + "allow_workflow":true + } + } + ] + }, + "metadata":"{\"instance\":{\"spot\":{\"termination_time\":\"now\"}}}" +} +` + // tinkerbellFilterMetadata is used for testing the filterMetadata function and has the 'metadata' field represented as an object (as opposed to string) + tinkerbellFilterMetadata = `{"id":"0eba0bf8-3772-4b4a-ab9f-6ebe93b90a94","metadata":{"components":{"id":"bc9ce39b-7f18-425b-bc7b-067914fa9786","type":"DiskComponent"},"instance":{"api_url":"https://metadata.packet.net","class":"c3.small.x86","customdata":{},"facility":"sjc1","hostname":"tink-provisioner","id":"7c9a5711-aadd-4fa0-8e57-789431626a27","iqn":"iqn.2020-06.net.packet:device.7c9a5711","network":{"addresses":[{"address":"139.175.86.114","address_family":4,"cidr":31,"created_at":"2020-06-19T04:16:08Z","enabled":true,"gateway":"139.175.86.113","id":"99e15f8e-6eab-40db-9c6f-69a69ef9854f","management":true,"netmask":"255.255.255.254","network":"139.175.86.113","parent_block":{"cidr":31,"href":"/ips/179580b0-3ae4-4fc0-8cbe-4f34174bebb4","netmask":"255.255.255.254","network":"139.175.86.113"},"public":true},{"address":"2604:1380:1000:ca00::7","address_family":6,"cidr":127,"created_at":"2020-06-19T04:16:08Z","enabled":true,"gateway":"2604:1380:1000:ca00::6","id":"f4b24331-c6cf-4ae4-899b-e78f223b2c57","management":true,"netmask":"ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe","network":"2604:1380:1000:ca00::6","parent_block":{"cidr":56,"href":"/ips/960aa63d-eeb6-410e-8242-1d6e2e7733fc","netmask":"ffff:ffff:ffff:ff00:0000:0000:0000:0000","network":"2604:1380:1000:ca00:0000:0000:0000:0000"},"public":true},{"address":"10.87.63.3","address_family":4,"cidr":31,"created_at":"2020-06-19T04:16:08Z","enabled":true,"gateway":"10.87.63.2","id":"5cca13a9-43d0-45a6-9ed7-3d9e2fbf0e87","management":true,"netmask":"255.255.255.254","network":"10.87.63.2","parent_block":{"cidr":25,"href":"/ips/7cde0a1b-d787-4a10-9c96-4049c7d5eeb3","netmask":"255.255.255.128","network":"10.87.63.0"},"public":false}],"bonding":{"link_aggregation":null,"mac":"b4:96:91:5f:ad:d8","mode":4},"interfaces":[{"bond":"bond0","mac":"b4:96:91:5f:ad:d8","name":"eth0"},{"bond":"bond0","mac":"b4:96:91:5f:ad:d9","name":"eth1"}]},"operating_system":{"distro":"ubuntu","image_tag":"f8f0331d31935319dfa8b6d551b5680840d7944f","license_activation":{"state":"unlicensed"},"slug":"ubuntu_18_04","version":"18.04"},"phone_home_url":"http://tinkerbell.sjc1.packet.net/phone-home","plan":"c3.small.x86","private_subnets":["10.0.0.0/8"],"specs":{"cpus":[{"count":1,"type":"EPYC 3151 4 Core Processor @ 2.7GHz"}],"drives":[{"category":"boot","count":2,"size":"240GB","type":"SSD"}],"features":{},"memory":{"total":"16GB"},"nics":[{"count":2,"type":"10Gbps"}]},"spot":{},"ssh_keys":[],"storage":{"disks":[{"device":"/dev/sda","partitions":[{"label":"BIOS","number":1,"size":4096},{"label":"SWAP","number":2,"size":"3993600"},{"label":"ROOT","number":3,"size":0}],"wipeTable":true}],"filesystems":[{"mount":{"create":{"options":["-L","ROOT"]},"device":"/dev/sda3","format":"ext4","point":"/"}},{"mount":{"create":{"options":["-L","SWAP"]},"device":"/dev/sda2","format":"swap","point":"none"}}]},"switch_short_id":"68c7fa13","tags":["hello","test"],"user_state_url":"http://tinkerbell.sjc1.packet.net/events","volumes":[]},"userdata":"#!/bin/bash\n\necho \"Hello world!\""},"network":{"interfaces":[{"dhcp":{"arch":"x86_64","ip":{"address":"192.168.1.5","gateway":"192.168.1.1","netmask":"255.255.255.248"},"mac":"b4:96:91:5f:af:c0"},"netboot":{"allow_pxe":true,"allow_workflow":true}}]}}` )