Skip to content

Commit

Permalink
Add HEAD request support to check if metadata/userdata exists
Browse files Browse the repository at this point in the history
This allows an authenticated user/service to check the existance of
metadata or userdata while generating minimal network traffic.

Signed-off-by: Scott Garman <[email protected]>
  • Loading branch information
ScottGarman committed Apr 18, 2023
1 parent bee5de4 commit 2dc28f6
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 18 deletions.
11 changes: 1 addition & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
Expand Down Expand Up @@ -423,8 +422,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
Expand Down Expand Up @@ -522,8 +521,6 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
Expand All @@ -536,17 +533,13 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
Expand Down Expand Up @@ -1221,8 +1214,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func (r *Router) Routes(rg *gin.RouterGroup) {
rg.POST(InternalMetadataURI, authMw.AuthRequired(), authMw.RequiredScopes(upsertScopes("metadata")), r.instanceMetadataSet)
rg.POST(InternalUserdataURI, authMw.AuthRequired(), authMw.RequiredScopes(upsertScopes("userdata")), r.instanceUserdataSet)

rg.HEAD(InternalMetadataWithIDURI, authMw.AuthRequired(), authMw.RequiredScopes(readScopes("metadata")), r.instanceMetadataExistsInternal)
rg.HEAD(InternalUserdataWithIDURI, authMw.AuthRequired(), authMw.RequiredScopes(readScopes("userdata")), r.instanceUserdataExistsInternal)

rg.GET(InternalMetadataWithIDURI, authMw.AuthRequired(), authMw.RequiredScopes(readScopes("metadata")), r.instanceMetadataGetInternal)
rg.GET(InternalUserdataWithIDURI, authMw.AuthRequired(), authMw.RequiredScopes(readScopes("userdata")), r.instanceUserdataGetInternal)
rg.DELETE(InternalMetadataWithIDURI, authMw.AuthRequired(), authMw.RequiredScopes(deleteScopes("metadata")), r.instanceMetadataDelete)
Expand Down
64 changes: 62 additions & 2 deletions pkg/api/v1/router_instance_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package metadataservice

import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/volatiletech/null/v8"
Expand Down Expand Up @@ -91,7 +93,6 @@ func (r *Router) instanceMetadataGetInternal(c *gin.Context) {

if err != nil {
invalidUUIDResponse(c, err)

return
}

Expand All @@ -116,6 +117,40 @@ func (r *Router) instanceMetadataGetInternal(c *gin.Context) {
}
}

// instanceMetadataExistsInternal retrieves the requested instance ID from the
// path and looks to see if the database has metadata recorded for that ID.
// If so, it returns a 200. If not, it returns a 404. This can be used by an
// authenticated external system to determine which instances the metadata
// service already knows about with minimal network overhead.
func (r *Router) instanceMetadataExistsInternal(c *gin.Context) {
instanceID, err := getUUIDParam(c, "instance-id")

if err != nil {
invalidUUIDResponse(c, err)
return
}

metadata, err := models.FindInstanceMetadatum(c.Request.Context(), r.DB, instanceID)

if err != nil {
c.Status(http.StatusNotFound)
return
}

// HEAD request responses still set the Content-Length header to what it
// would be if we were returning the metadata
bytes, err := json.Marshal(metadata.Metadata)
if err != nil {
r.Logger.Warn("Error during json.Marshal() of metadata")
c.Status(http.StatusInternalServerError)

return
}

c.Writer.Header().Set("Content-Length", strconv.Itoa(len(bytes)))
c.Status(http.StatusOK)
}

func (r *Router) instanceUserdataGet(c *gin.Context) {
userdata, err := r.getUserdata(c)

Expand Down Expand Up @@ -145,7 +180,6 @@ func (r *Router) instanceUserdataGetInternal(c *gin.Context) {

if err != nil {
invalidUUIDResponse(c, err)

return
}

Expand All @@ -162,6 +196,32 @@ func (r *Router) instanceUserdataGetInternal(c *gin.Context) {
c.String(http.StatusOK, string(userdata.Userdata.Bytes))
}

// instanceUserdataExistsInternal retrieves the requested instance ID from the
// path and looks to see if the database has userdata recorded for that ID.
// If so, it returns a 200. If not, it will just return a 404. This can be use
// by an authenticated external system to determine which instances the userdata
// service already knows about with minimal network overhead.
func (r *Router) instanceUserdataExistsInternal(c *gin.Context) {
instanceID, err := getUUIDParam(c, "instance-id")

if err != nil {
invalidUUIDResponse(c, err)
return
}

userdata, err := models.FindInstanceUserdatum(c.Request.Context(), r.DB, instanceID)

if err != nil {
c.Status(http.StatusNotFound)
return
}

// HEAD request responses still set the Content-Length header to what it
// would be if we were returning the userdata
c.Writer.Header().Set("Content-Length", strconv.Itoa(len(userdata.Userdata.Bytes)))
c.Status(http.StatusOK)
}

// There's a few steps we need to perform when upserting both instance_metadata
// and instance_userdata:
// 0. Validate the request body
Expand Down
37 changes: 33 additions & 4 deletions pkg/api/v1/router_instance_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,13 @@ func TestDeleteMetadata(t *testing.T) {
}
}

// metadataString is a helper function that ensures the db fixture string is marshaled
// in a way that we can properly calculate its length for Content-Length comparisons
func metadataString(metadata interface{}) string {
b, _ := json.Marshal(metadata)
return string(b)
}

func TestGetMetadataInternal(t *testing.T) {
router := *testHTTPServer(t)

Expand Down Expand Up @@ -632,25 +639,25 @@ func TestGetMetadataInternal(t *testing.T) {
"Instance A",
dbtools.FixtureInstanceA.InstanceID,
http.StatusOK,
dbtools.FixtureInstanceA.InstanceMetadata.Metadata.String(),
metadataString(dbtools.FixtureInstanceA.InstanceMetadata.Metadata),
},
{
"Instance B",
dbtools.FixtureInstanceB.InstanceID,
http.StatusOK,
dbtools.FixtureInstanceB.InstanceMetadata.Metadata.String(),
metadataString(dbtools.FixtureInstanceB.InstanceMetadata.Metadata),
},
{
"Instance C",
dbtools.FixtureInstanceC.InstanceID,
http.StatusOK,
dbtools.FixtureInstanceC.InstanceMetadata.Metadata.String(),
metadataString(dbtools.FixtureInstanceC.InstanceMetadata.Metadata),
},
{
"Instance D",
dbtools.FixtureInstanceD.InstanceID,
http.StatusOK,
dbtools.FixtureInstanceD.InstanceMetadata.Metadata.String(),
metadataString(dbtools.FixtureInstanceD.InstanceMetadata.Metadata),
},
// Instance E does not have metadata, so we'd expect a 404
{
Expand All @@ -668,6 +675,28 @@ func TestGetMetadataInternal(t *testing.T) {
},
}

// HEAD request tests
for _, testcase := range testCases {
t.Run(testcase.testName, func(t *testing.T) {
w := httptest.NewRecorder()

req, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, v1api.GetInternalMetadataByIDPath(testcase.instanceID), nil)
router.ServeHTTP(w, req)
response := w.Result()

assert.Equal(t, testcase.expectedStatus, w.Code)

if w.Code == 200 {
// HEAD responses should have an empty body, but set the Content-Length
// header to what the response body would otherwise be for a GET request
assert.Zero(t, w.Body.Len())
assert.Equal(t, int64(len(testcase.expectedBody)), response.ContentLength)
response.Body.Close()
}
})
}

// GET request tests
for _, testcase := range testCases {
t.Run(testcase.testName, func(t *testing.T) {
w := httptest.NewRecorder()
Expand Down
22 changes: 22 additions & 0 deletions pkg/api/v1/router_instance_userdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,28 @@ func TestGetUserdataInternal(t *testing.T) {
},
}

// HEAD request tests
for _, testcase := range testCases {
t.Run(testcase.testName, func(t *testing.T) {
w := httptest.NewRecorder()

req, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, v1api.GetInternalUserdataByIDPath(testcase.instanceID), nil)
router.ServeHTTP(w, req)
response := w.Result()

assert.Equal(t, testcase.expectedStatus, w.Code)

if w.Code == 200 {
// HEAD responses should have an empty body, but set the Content-Length
// header to what the response body would otherwise be for a GET request
assert.Zero(t, w.Body.Len())
assert.Equal(t, int64(len(testcase.expectedBody)), response.ContentLength)
response.Body.Close()
}
})
}

// GET request tests
for _, testcase := range testCases {
t.Run(testcase.testName, func(t *testing.T) {
w := httptest.NewRecorder()
Expand Down
4 changes: 2 additions & 2 deletions quickstart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.9"

services:
metadataservice:
image: ghcr.io/metal-toolbox/hollow-metadaaservice:v0.0.8
image: ghcr.io/metal-toolbox/hollow-metadaaservice:v0.0.11
depends_on:
crdb:
condition: service_healthy
Expand All @@ -18,7 +18,7 @@ services:
- metadataservice

metadataservice-migrate:
image: ghcr.io/metal-toolbox/hollow-metadataservice:v0.0.8
image: ghcr.io/metal-toolbox/hollow-metadataservice:v0.0.11
command:
migrate up
depends_on:
Expand Down

0 comments on commit 2dc28f6

Please sign in to comment.