From 4b48ff9180fba55cd3f00a2332b615b55bbacf57 Mon Sep 17 00:00:00 2001 From: salrashid123 Date: Thu, 21 Mar 2024 11:02:09 -0400 Subject: [PATCH] add configFile listener; ETag support --- README.md | 53 ++++++++++++----- cmd/BUILD.bazel | 1 + cmd/main.go | 51 ++++++++++++++++ go.mod | 1 + go.sum | 2 + repositories.bzl | 9 ++- server.go | 150 +++++++++++++++++++++++++++++++++-------------- 7 files changed, 207 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 74dd7a6..103155b 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ r.Handle("/") - [Run with containers](#run-with-containers) - [Running as Kubernetes Service](#running-as-kubernetes-service) - [Static environment variables](#static-environment-variables) +- [Dynamic Configuration File Updates](#dynamic-configuration-file-updates) +- [ETag](#etag) - [Extending the sample](#extending-the-sample) - [Using link-local address](#using-link-local-address) - [Using domain sockets](#using-domain-sockets) @@ -608,22 +610,7 @@ This emulator is also published as a release-tagged container to dockerhub: * [https://hub.docker.com/r/salrashid123/gcemetadataserver](https://hub.docker.com/r/salrashid123/gcemetadataserver) -The images are also signed using my github address (`salrashid123@gmail`). If you really want to, you can verify each signature usign `cosign`: - -```bash -## for tag/version 3.4.0: -IMAGE="index.docker.io/salrashid123/gcemetadataserver@sha256:c3cec9e18adb87a14889f19ab0c3c87d66339284b35ca72135ff9dcd58a59671" - -## i signed it directly, keyless: -# $ cosign sign $IMAGE - -## which you can verify: -$ cosign verify --certificate-identity=salrashid123@gmail.com --certificate-oidc-issuer=https://github.com/login/oauth $IMAGE | jq '.' - -## search and get -# $ rekor-cli search --rekor_server https://rekor.sigstore.dev --email salrashid123@gmail.com -# $ rekor-cli get --rekor_server https://rekor.sigstore.dev --log-index $LogIndex --format=json | jq '.' -``` +You can verify the image were signed by the repo owner if you really want to (see section below). ### Run with containers @@ -696,6 +683,21 @@ Number of Buckets: 62 >> needless to say, the metadata Service should be accessed only form authorized pods +### Dynamic Configuration File Updates + +Changes to the claims configuration file (`--configFile=`) while the metadata server is running will automatically update values returned by the server. + +On startup, the metadata server sets a file listener on that config file and any updates to the values will propagate back to the server without requiring a restart. + +### ETag + +GCE metadata servers return values with [ETag](https://cloud.google.com/compute/docs/metadata/querying-metadata#etags) headers. The ETag is used to check if a specific attribute or value has changed. + +This metadata server will hash the value for the body to return and use that as the ETag. If you update the configuration file with new attributes or values, the ETag for that node will change. The `ETag` header key is returned in non-canonical format. + +Note `wait-for-change` value is not supported currently so while you can poll for etag changes, you cannot listen and hold. + + ### Static environment variables If you do not have access to certificate file or would like to specify **static** token values via env-var, the metadata server supports the following environment variables as substitutions. Once you set these environment variables, the service will not look for anything using the service Account JSON file (even if specified) @@ -973,6 +975,25 @@ wget https://github.com/salrashid123/gce_metadata_server/releases/download/v3.4. gpg --verify gce_metadata_server_3.4.1_checksums.txt.sig gce_metadata_server_3.4.1_checksums.txt ``` +#### Verify Container Image Signature + +The images are also signed using my github address (`salrashid123@gmail`). If you really want to, you can verify each signature usign `cosign`: + +```bash +## for tag/version 3.4.0: +IMAGE="index.docker.io/salrashid123/gcemetadataserver@sha256:c3cec9e18adb87a14889f19ab0c3c87d66339284b35ca72135ff9dcd58a59671" + +## i signed it directly, keyless: +# $ cosign sign $IMAGE + +## which you can verify: +$ cosign verify --certificate-identity=salrashid123@gmail.com --certificate-oidc-issuer=https://github.com/login/oauth $IMAGE | jq '.' + +## search and get +# $ rekor-cli search --rekor_server https://rekor.sigstore.dev --email salrashid123@gmail.com +# $ rekor-cli get --rekor_server https://rekor.sigstore.dev --log-index $LogIndex --format=json | jq '.' +``` + ## Testing a lot todo here, right...thats just life diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index 5eca24a..b9edac8 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "@com_google_cloud_go_iam//credentials/apiv1:go_default_library", "@com_github_google_go_tpm//legacy/tpm2:go_default_library", "@com_github_golang_glog//:go_default_library", + "@com_github_fsnotify_fsnotify//:go_default_library", ], ) diff --git a/cmd/main.go b/cmd/main.go index 114b376..1a7dbe0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,8 +6,11 @@ import ( "flag" "os" "os/signal" + "path/filepath" "syscall" + "time" + "github.com/fsnotify/fsnotify" "github.com/golang/glog" "github.com/google/go-tpm/legacy/tpm2" mds "github.com/salrashid123/gce_metadata_server" @@ -156,6 +159,54 @@ func main() { os.Exit(1) } + watcher, err := fsnotify.NewWatcher() + if err != nil { + glog.Errorf("Error creating file watcher: %v\n", err) + os.Exit(1) + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if event.Has(fsnotify.Write) { + if event.Name == *configFile { + time.Sleep(8 * time.Millisecond) // https://github.com/fsnotify/fsnotify/issues/372 + configData, err := os.ReadFile(*configFile) + if err != nil { + glog.Errorf("Error reading configFile: %v\n", err) + return + } + + claims := &mds.Claims{} + err = json.Unmarshal(configData, claims) + if err != nil { + glog.Errorf("Error parsing json: %v\n", err) + return + } + f.Claims = *claims + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + glog.Errorf("Error on filewatcher %v\n", err) + } + } + }() + + err = watcher.Add(filepath.Dir(*configFile)) + if err != nil { + glog.Errorf("Error watching configFile: %v\n", err) + os.Exit(1) + } + done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) diff --git a/go.mod b/go.mod index 51ab98d..69dcc30 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( cloud.google.com/go/compute/metadata v0.2.3 cloud.google.com/go/iam v1.1.5 + github.com/fsnotify/fsnotify v1.7.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/glog v1.2.0 github.com/google/go-tpm v0.9.0 diff --git a/go.sum b/go.sum index bf5f2c9..e5e0d76 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/repositories.bzl b/repositories.bzl index f1626f0..871ffc5 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -67,6 +67,13 @@ def go_repositories(): sum = "h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=", version = "v1.0.4", ) + go_repository( + name = "com_github_fsnotify_fsnotify", + importpath = "github.com/fsnotify/fsnotify", + sum = "h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=", + version = "v1.7.0", + ) + go_repository( name = "com_github_go_logr_logr", importpath = "github.com/go-logr/logr", @@ -201,8 +208,8 @@ def go_repositories(): ) go_repository( name = "com_github_googleapis_gax_go_v2", + build_file_proto_mode = "disable_global", importpath = "github.com/googleapis/gax-go/v2", - build_file_proto_mode = "disable_global", sum = "h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=", version = "v2.12.0", ) diff --git a/server.go b/server.go index 5f3b400..c1ab770 100644 --- a/server.go +++ b/server.go @@ -23,6 +23,7 @@ package mds import ( "bytes" + "crypto/md5" "encoding/json" "errors" "io" @@ -285,6 +286,12 @@ func (h *MetadataServer) pathListFields(b interface{}) string { return resp } +func getETag(body []byte) string { + hash := md5.Sum(body) + etag := fmt.Sprintf("%x", hash[8:]) + return etag +} + func (h *MetadataServer) rootHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/text") resp := h.pathListFields(h.Claims) @@ -299,7 +306,7 @@ func (h *MetadataServer) notFound(w http.ResponseWriter, r *http.Request) { func (h *MetadataServer) computeMetadataHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/text") resp := h.pathListFields(h.Claims.ComputeMetadata) - fmt.Fprint(w, resp) + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1Handler(w http.ResponseWriter, r *http.Request) { @@ -308,7 +315,9 @@ func (h *MetadataServer) computeMetadatav1Handler(w http.ResponseWriter, r *http } w.Header().Set("Content-Type", "application/text") resp := h.pathListFields(h.Claims.ComputeMetadata.V1) - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1ProjectHandler(w http.ResponseWriter, r *http.Request) { @@ -317,26 +326,36 @@ func (h *MetadataServer) computeMetadatav1ProjectHandler(w http.ResponseWriter, } w.Header().Set("Content-Type", "application/text") resp := h.pathListFields(h.Claims.ComputeMetadata.V1.Project) - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1ProjectProjectIDHandler(w http.ResponseWriter, r *http.Request) { + var resp []byte w.Header().Set("Content-Type", "application/text") if os.Getenv(googleProjectID) != "" { - fmt.Fprint(w, os.Getenv(googleProjectID)) + resp = []byte(os.Getenv(googleProjectID)) } else { - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Project.ProjectID) + resp = []byte(h.Claims.ComputeMetadata.V1.Project.ProjectID) } + e := getETag(resp) + w.Header()["ETag"] = []string{e} + w.Write(resp) } func (h *MetadataServer) computeMetadatav1ProjectNumericProjectIDHandler(w http.ResponseWriter, r *http.Request) { + var resp []byte w.Header().Set("Content-Type", "application/text") if os.Getenv(googleProjectNumber) != "" { - fmt.Fprint(w, os.Getenv(googleProjectNumber)) + resp = []byte(os.Getenv(googleProjectNumber)) } else { - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Project.NumericProjectID) + resp = []byte(strconv.FormatInt(h.Claims.ComputeMetadata.V1.Project.NumericProjectID, 10)) } + e := getETag(resp) + w.Header()["ETag"] = []string{e} + w.Write(resp) } func (h *MetadataServer) handleRecursion(w http.ResponseWriter, r *http.Request, s interface{}) bool { @@ -349,6 +368,8 @@ func (h *MetadataServer) handleRecursion(w http.ResponseWriter, r *http.Request, return true } w.Header().Set("Content-Type", "application/json") + e := getETag(jsonResponse) + w.Header()["ETag"] = []string{e} w.WriteHeader(http.StatusOK) w.Write(jsonResponse) return true @@ -361,7 +382,7 @@ func (h *MetadataServer) handleBasePathRedirect(w http.ResponseWriter, r *http.R w.Header().Set("Location", fmt.Sprintf("http://%s%s/", r.Host, r.RequestURI)) w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusMovedPermanently) - fmt.Fprint(w, fmt.Sprintf("%s/\n", r.RequestURI)) + w.Write([]byte(fmt.Sprintf("%s/\n", r.RequestURI))) } func (h *MetadataServer) computeMetadatav1ProjectAttributesHandler(w http.ResponseWriter, r *http.Request) { @@ -373,7 +394,10 @@ func (h *MetadataServer) computeMetadatav1ProjectAttributesHandler(w http.Respon keys = keys + k + "\n" } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, keys) + + e := getETag([]byte(keys)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(keys)) } func (h *MetadataServer) computeMetadatav1ProjectAttributesKeyHandler(w http.ResponseWriter, r *http.Request) { @@ -381,8 +405,10 @@ func (h *MetadataServer) computeMetadatav1ProjectAttributesKeyHandler(w http.Res // todo: ?alt=json returns content-type=application/json but the payload is text.. vars := mux.Vars(r) if val, ok := h.Claims.ComputeMetadata.V1.Project.Attributes[vars["key"]]; ok { + e := getETag([]byte(val)) + w.Header()["ETag"] = []string{e} w.WriteHeader(http.StatusOK) - fmt.Fprint(w, val) + w.Write([]byte(val)) } else { w.WriteHeader(http.StatusNotFound) w.Header().Set("Content-Type", "text/html; charset=UTF-8") @@ -391,18 +417,19 @@ func (h *MetadataServer) computeMetadatav1ProjectAttributesKeyHandler(w http.Res } func (h *MetadataServer) getServiceAccountHandler(w http.ResponseWriter, r *http.Request) { + var resp []byte vars := mux.Vars(r) switch vars["key"] { case "aliases": w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, "default") + resp = []byte("default") case "email": w.Header().Set("Content-Type", "application/text") if os.Getenv(googleServiceAccountEmail) != "" { - fmt.Fprint(w, os.Getenv(googleServiceAccountEmail)) + resp = []byte(os.Getenv(googleServiceAccountEmail)) } else { - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.ServiceAccounts["default"].Email) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.ServiceAccounts["default"].Email) } case "identity": k, ok := r.URL.Query()["audience"] @@ -418,13 +445,14 @@ func (h *MetadataServer) getServiceAccountHandler(w http.ResponseWriter, r *http } w.Header().Set("Content-Type", "text/html") fmt.Fprint(w, idtok) + return case "scopes": var scopes string for _, e := range h.Claims.ComputeMetadata.V1.Instance.ServiceAccounts["default"].Scopes { scopes = scopes + e + "\n" } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, scopes) + resp = []byte(scopes) case "token": tok, err := h.getAccessToken() if err != nil { @@ -440,12 +468,16 @@ func (h *MetadataServer) getServiceAccountHandler(w http.ResponseWriter, r *http } w.Header().Set("Content-Type", "application/json") w.Write(js) + return default: httpError(w, http.StatusText(http.StatusNotFound), http.StatusNotFound, "text/html; charset=UTF-8") return } + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) getAccessToken() (*metadataToken, error) { @@ -760,7 +792,9 @@ func (h *MetadataServer) listServiceAccountsIndexHandler(w http.ResponseWriter, keys = keys + k + "\n" } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, keys) + e := getETag([]byte(keys)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(keys)) } func (h *MetadataServer) listServiceAccountHandler(w http.ResponseWriter, r *http.Request) { @@ -770,7 +804,9 @@ func (h *MetadataServer) listServiceAccountHandler(w http.ResponseWriter, r *htt } keys := h.pathListFields(h.Claims.ComputeMetadata.V1.Instance.ServiceAccounts[vars["acct"]]) w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, keys) + e := getETag([]byte(keys)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(keys)) } func (h *MetadataServer) computeMetadatav1InstanceHandler(w http.ResponseWriter, r *http.Request) { @@ -779,38 +815,46 @@ func (h *MetadataServer) computeMetadatav1InstanceHandler(w http.ResponseWriter, } resp := h.pathListFields(h.Claims.ComputeMetadata.V1.Instance) w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) + } func (h *MetadataServer) computeMetadatav1InstanceKeyHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) + + var res []byte + var err error + + // default content-type + w.Header().Set("Content-Type", "application/text") switch vars["key"] { case "id": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.ID) + res = []byte(strconv.FormatInt(h.Claims.ComputeMetadata.V1.Instance.ID, 10)) case "name": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.Name) + res = []byte(h.Claims.ComputeMetadata.V1.Instance.Name) case "hostname": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.Hostname) + res = []byte(h.Claims.ComputeMetadata.V1.Instance.Hostname) case "zone": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.Zone) + res = []byte(h.Claims.ComputeMetadata.V1.Instance.Zone) case "machine-type": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.MachineType) + res = []byte(h.Claims.ComputeMetadata.V1.Instance.MachineType) case "tags": - jsonResponse, err := json.Marshal(h.Claims.ComputeMetadata.V1.Instance.Tags) + res, err = json.Marshal(h.Claims.ComputeMetadata.V1.Instance.Tags) if err != nil { glog.Errorf("Error converting value to JSON %v\n", err) httpError(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, "text/plain; charset=UTF-8") return } w.Header().Set("Content-Type", "application/json") - w.Write(jsonResponse) - return default: httpError(w, http.StatusText(http.StatusNotFound), http.StatusNotFound, "text/html; charset=UTF-8") return } - w.Header().Set("Content-Type", "application/text") - return + e := getETag(res) + w.Header()["ETag"] = []string{e} + w.Write(res) } func (h *MetadataServer) computeMetadatav1InstanceAttributesHandler(w http.ResponseWriter, r *http.Request) { @@ -822,7 +866,10 @@ func (h *MetadataServer) computeMetadatav1InstanceAttributesHandler(w http.Respo keys = keys + k + "\n" } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, keys) + + e := getETag([]byte(keys)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(keys)) } func (h *MetadataServer) computeMetadatav1InstanceAttributesKeyHandler(w http.ResponseWriter, r *http.Request) { @@ -831,8 +878,10 @@ func (h *MetadataServer) computeMetadatav1InstanceAttributesKeyHandler(w http.Re } vars := mux.Vars(r) if val, ok := h.Claims.ComputeMetadata.V1.Instance.Attributes[vars["key"]]; ok { + e := getETag([]byte(val)) + w.Header()["ETag"] = []string{e} w.WriteHeader(http.StatusOK) - fmt.Fprint(w, val) + w.Write([]byte(val)) } else { w.WriteHeader(http.StatusNotFound) w.Header().Set("Content-Type", "text/html; charset=UTF-8") @@ -849,7 +898,9 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkHandler(w http.Response resp = resp + fmt.Sprintf("%d/\n", i) } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceHandler(w http.ResponseWriter, r *http.Request) { @@ -868,10 +919,13 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceHandler(w http } resp := h.pathListFields(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i]) w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceKeyHandler(w http.ResponseWriter, r *http.Request) { + var resp []byte vars := mux.Vars(r) i, err := strconv.Atoi(vars["index"]) if err != nil { @@ -888,30 +942,33 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceKeyHandler(w h // return case "dns-servers": // gce metadata server default returns "application/text" for dns-servers - fmt.Fprint(w, strings.Join(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].DNSServers, "\n")) + resp = []byte(strings.Join(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].DNSServers, "\n")) case "forwarded-ips": h.handleBasePathRedirect(w, r) return case "gateway": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Gateway) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Gateway) case "ip": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].IP) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].IP) case "ip-aliases": h.handleBasePathRedirect(w, r) return case "mac": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Mac) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Mac) case "mtu": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Mtu) + resp = []byte(strconv.Itoa(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Mtu)) case "network": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Network) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Network) case "subnet-mask": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Subnetmask) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].Subnetmask) default: httpError(w, metadata404Body, http.StatusNotFound, "text/html; charset=UTF-8") return } w.Header().Set("Content-Type", "application/text") + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsHandler(w http.ResponseWriter, r *http.Request) { @@ -933,7 +990,9 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsH resp = resp + fmt.Sprintf("%d/\n", i) } w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsIndexHandler(w http.ResponseWriter, r *http.Request) { @@ -963,7 +1022,9 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsI resp := h.pathListFields(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].AccessConfigs[k]) w.Header().Set("Content-Type", "application/text") - fmt.Fprint(w, resp) + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsIndexRedirectHandler(w http.ResponseWriter, r *http.Request) { @@ -990,6 +1051,7 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsI } func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsKeyHandler(w http.ResponseWriter, r *http.Request) { + var resp []byte vars := mux.Vars(r) i, err := strconv.Atoi(vars["index"]) if err != nil { @@ -1013,15 +1075,17 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsK switch vars["key"] { case "external-ip": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].AccessConfigs[k].ExternalIP) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].AccessConfigs[k].ExternalIP) case "type": - fmt.Fprint(w, h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].AccessConfigs[k].Type) + resp = []byte(h.Claims.ComputeMetadata.V1.Instance.NetworkInterfaces[i].AccessConfigs[k].Type) default: httpError(w, metadata404Body, http.StatusNotFound, "text/html; charset=UTF-8") return } w.Header().Set("Content-Type", "application/text") - + e := getETag([]byte(resp)) + w.Header()["ETag"] = []string{e} + w.Write([]byte(resp)) } // Start running the metadata server using the configuration provided through `NewMetadataServer()`