-
Notifications
You must be signed in to change notification settings - Fork 13
feat(api): POST /environment/reload applies the guest metadata environment live #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
d4d1409
3832aa6
6b36cc0
96c39e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| package handler | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "os" | ||
| "sync" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // defaultMetadataPath is the guest metadata document served by the host | ||
| // (vmm-manager) through the initrd's FUSE mount. It always reflects the | ||
| // current generation: on an environment update the host persists the new set, | ||
| // the initrd refetches the document, then pings POST /environment/reload so | ||
| // this process adopts it. Absent on VMs booted from an initrd that predates | ||
| // the metadata protocol. Overridable through BL_METADATA_PATH. | ||
| const defaultMetadataPath = "/bl/metadata" | ||
|
|
||
| func metadataPath() string { | ||
| if path := os.Getenv("BL_METADATA_PATH"); path != "" { | ||
| return path | ||
| } | ||
| return defaultMetadataPath | ||
| } | ||
|
|
||
| // metadataDocument is the subset of the guest metadata document this handler | ||
| // reads. The environment carried is the host's complete set, so a variable the | ||
| // host no longer has must be unset here too. | ||
| type metadataDocument struct { | ||
| Generation int64 `json:"generation"` | ||
| Environment map[string]string `json:"environment"` | ||
| } | ||
|
|
||
| // EnvironmentHandler applies environment updates to the sandbox-api process | ||
| // itself. A live process's environment cannot be changed from outside, so the | ||
| // initrd notifies this endpoint after applying a new metadata generation; the | ||
| // values are set with os.Setenv, which also makes every process spawned | ||
| // afterwards (process API, terminals, restarts) inherit them. | ||
| type EnvironmentHandler struct { | ||
| *BaseHandler | ||
|
|
||
| mu sync.Mutex | ||
| // Keys applied from the metadata document, so a key the next generation no | ||
| // longer carries is unset rather than left behind. Keys that were never in | ||
| // the document (the image ENV, boot-time variables) are never touched. | ||
| applied map[string]struct{} | ||
| } | ||
|
|
||
| // NewEnvironmentHandler creates a new environment handler. | ||
| func NewEnvironmentHandler() *EnvironmentHandler { | ||
| return &EnvironmentHandler{ | ||
| BaseHandler: NewBaseHandler(), | ||
| applied: map[string]struct{}{}, | ||
| } | ||
| } | ||
|
|
||
| // ReloadResponse is the response body for the environment reload endpoint. | ||
| type ReloadResponse struct { | ||
| Generation int64 `json:"generation"` | ||
| Applied int `json:"applied"` | ||
| Removed int `json:"removed"` | ||
| } | ||
|
|
||
| // HandleReload reloads the environment from the guest metadata document | ||
| // @Summary Reload environment from guest metadata | ||
| // @Description Re-reads /bl/metadata and applies its environment to the sandbox-api process, so this process and every process started afterwards see the current values. Called by the guest init after an environment update; safe to call manually. | ||
| // @Tags system | ||
| // @Produce json | ||
| // @Success 200 {object} ReloadResponse | ||
| // @Failure 404 {object} ErrorResponse | ||
| // @Failure 500 {object} ErrorResponse | ||
| // @Router /environment/reload [post] | ||
| func (h *EnvironmentHandler) HandleReload(c *gin.Context) { | ||
| raw, err := os.ReadFile(metadataPath()) | ||
| if err != nil { | ||
| if os.IsNotExist(err) { | ||
| h.SendError(c, http.StatusNotFound, err) | ||
| return | ||
| } | ||
| h.SendError(c, http.StatusInternalServerError, err) | ||
| return | ||
| } | ||
|
|
||
| var doc metadataDocument | ||
| if err := json.Unmarshal(raw, &doc); err != nil { | ||
| h.SendError(c, http.StatusInternalServerError, err) | ||
| return | ||
| } | ||
|
|
||
| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
|
|
||
| removed := 0 | ||
| for key := range h.applied { | ||
| if _, ok := doc.Environment[key]; !ok { | ||
| if err := os.Unsetenv(key); err == nil { | ||
| removed++ | ||
| } | ||
| delete(h.applied, key) | ||
| } | ||
| } | ||
| applied := 0 | ||
| for key, value := range doc.Environment { | ||
| if err := os.Setenv(key, value); err != nil { | ||
| logrus.WithError(err).WithField("key", key).Warn("Failed to set environment variable") | ||
| continue | ||
| } | ||
| h.applied[key] = struct{}{} | ||
| applied++ | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
Comment on lines
+101
to
+118
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Variables that also exist in the image configuration are erased instead of restored when dropped from metadata A variable that the container image already defined is deleted outright ( No snapshot of pre-existing values before overwriting
Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 6b36cc0 — |
||
|
|
||
| logrus.WithFields(logrus.Fields{ | ||
| "generation": doc.Generation, | ||
| "applied": applied, | ||
| "removed": removed, | ||
| }).Info("Environment reloaded from guest metadata") | ||
|
|
||
| c.JSON(http.StatusOK, ReloadResponse{ | ||
| Generation: doc.Generation, | ||
| Applied: applied, | ||
| Removed: removed, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package handler | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| func performReload(t *testing.T, h *EnvironmentHandler) *httptest.ResponseRecorder { | ||
| t.Helper() | ||
| gin.SetMode(gin.TestMode) | ||
| w := httptest.NewRecorder() | ||
| c, _ := gin.CreateTestContext(w) | ||
| c.Request = httptest.NewRequest(http.MethodPost, "/environment/reload", nil) | ||
| h.HandleReload(c) | ||
| return w | ||
| } | ||
|
|
||
| func TestHandleReloadAppliesAndRemoves(t *testing.T) { | ||
| doc := filepath.Join(t.TempDir(), "metadata") | ||
| t.Setenv("BL_METADATA_PATH", doc) | ||
|
|
||
| h := NewEnvironmentHandler() | ||
| t.Cleanup(func() { | ||
| os.Unsetenv("RELOAD_TEST_A") | ||
| os.Unsetenv("RELOAD_TEST_B") | ||
| }) | ||
|
|
||
| if err := os.WriteFile(doc, []byte(`{"generation":1,"environment":{"RELOAD_TEST_A":"1","RELOAD_TEST_B":"2"}}`), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if w := performReload(t, h); w.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) | ||
| } | ||
| if os.Getenv("RELOAD_TEST_A") != "1" || os.Getenv("RELOAD_TEST_B") != "2" { | ||
| t.Fatalf("environment not applied") | ||
| } | ||
|
|
||
| // A variable the next generation no longer carries is unset; one it still | ||
| // carries is updated. | ||
| if err := os.WriteFile(doc, []byte(`{"generation":2,"environment":{"RELOAD_TEST_A":"updated"}}`), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if w := performReload(t, h); w.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) | ||
| } | ||
| if os.Getenv("RELOAD_TEST_A") != "updated" { | ||
| t.Fatalf("environment not updated") | ||
| } | ||
| if _, ok := os.LookupEnv("RELOAD_TEST_B"); ok { | ||
| t.Fatalf("removed variable still set") | ||
| } | ||
| } | ||
|
|
||
| func TestHandleReloadWithoutMetadata(t *testing.T) { | ||
| t.Setenv("BL_METADATA_PATH", filepath.Join(t.TempDir(), "missing")) | ||
|
|
||
| if w := performReload(t, NewEnvironmentHandler()); w.Code != http.StatusNotFound { | ||
| t.Fatalf("expected 404, got %d", w.Code) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Metadata can override the very variable that selects the metadata path
metadataPath()readsBL_METADATA_PATHfrom the process environment, andHandleReloadthen applies arbitrary key/values from the document viaos.Setenv. If a metadata document ever carriesBL_METADATA_PATH, subsequent reloads read from a different file (and if a later generation drops the key, the path silently reverts). Worth considering skipping this key when applying, or resolving the path once at handler construction.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6b36cc0 — the metadata path is now resolved once at handler construction, so a document carrying
BL_METADATA_PATHcan no longer redirect subsequent reloads.