diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go
index fedb7db2..e221d17b 100644
--- a/server/cmd/api/api/chromium.go
+++ b/server/cmd/api/api/chromium.go
@@ -108,7 +108,7 @@ func (s *ApiService) uploadExtensions(ctx context.Context, mr *multipart.Reader,
s.chromiumConfigMu.Lock()
defer s.chromiumConfigMu.Unlock()
- requiresRestart, transaction, reqMsg, err := s.commitPreparedExtensions(ctx, prepared)
+ transaction, reqMsg, err := s.commitPreparedExtensions(ctx, prepared)
if reqMsg != "" {
return badExtensionUpload(reqMsg)
}
@@ -116,7 +116,7 @@ func (s *ApiService) uploadExtensions(ctx context.Context, mr *multipart.Reader,
return internalExtensionUpload(err.Error())
}
- restarted := forceRestart || requiresRestart
+ restarted := forceRestart || prepared.requiresRestart
var loadErr error
if restarted {
if err := s.restartChromiumAndWait(ctx, "extension upload"); err != nil {
@@ -259,22 +259,25 @@ func (batch *preparedExtensionBatch) cleanup() {
}
}
-// applyExtensionZipItems installs name+zipTemp extension pairs and persists their startup
-// configuration. The boolean result reports whether enterprise policy requires a restart.
-func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensionZipItem) (bool, string, error) {
+// installExtensionZipItems extracts, validates, and persists extension archives.
+// The caller must hold chromiumConfigMu while it commits the prepared batch.
+func (s *ApiService) installExtensionZipItems(ctx context.Context, items []extensionZipItem) (string, error) {
+ if len(items) == 0 {
+ return "", nil
+ }
prepared, reqMsg, err := s.prepareExtensionZipItems(ctx, items)
if prepared != nil {
defer prepared.cleanup()
}
if reqMsg != "" || err != nil {
- return false, reqMsg, err
+ return reqMsg, err
}
- requiresRestart, _, reqMsg, err := s.commitPreparedExtensions(ctx, prepared)
- return requiresRestart, reqMsg, err
+ _, reqMsg, err = s.commitPreparedExtensions(ctx, prepared)
+ return reqMsg, err
}
-// prepareExtensionZipItems performs archive extraction and validation before the global Chromium
-// configuration lock is acquired. commitPreparedExtensions rechecks destination names under lock.
+// prepareExtensionZipItems extracts and validates archives into staging paths.
+// commitPreparedExtensions rechecks destination names while the caller holds the config lock.
func (s *ApiService) prepareExtensionZipItems(ctx context.Context, items []extensionZipItem) (*preparedExtensionBatch, string, error) {
log := logger.FromContext(ctx)
if err := os.MkdirAll(extensionsBaseDir, 0o755); err != nil {
@@ -438,22 +441,25 @@ func (batch *committedExtensionBatch) rollback() error {
return rollbackErr
}
-func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *preparedExtensionBatch) (requiresRestart bool, transaction *committedExtensionBatch, reqMsg string, err error) {
+// commitPreparedExtensions moves a prepared batch into place and updates policy and
+// flags, rolling back partial commit failures. Callers may retain the returned
+// transaction to roll back a later activation failure.
+func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *preparedExtensionBatch) (transaction *committedExtensionBatch, reqMsg string, err error) {
for _, extension := range batch.extensions {
if _, statErr := os.Stat(extension.finalPath); statErr == nil {
- return false, nil, fmt.Sprintf("extension name already exists: %s", extension.name), nil
+ return nil, fmt.Sprintf("extension name already exists: %s", extension.name), nil
} else if !os.IsNotExist(statErr) {
- return false, nil, "", fmt.Errorf("failed to check extension dir: %w", statErr)
+ return nil, "", fmt.Errorf("failed to check extension dir: %w", statErr)
}
}
flagsSnapshot, err := captureOptionalFileSnapshot(chromiumFlagsPath)
if err != nil {
- return false, nil, "", fmt.Errorf("failed to snapshot chromium flags: %w", err)
+ return nil, "", fmt.Errorf("failed to snapshot chromium flags: %w", err)
}
policySnapshot, err := captureOptionalFileSnapshot(policy.PolicyPath)
if err != nil {
- return false, nil, "", fmt.Errorf("failed to snapshot chromium policy: %w", err)
+ return nil, "", fmt.Errorf("failed to snapshot chromium policy: %w", err)
}
transaction = &committedExtensionBatch{
@@ -477,7 +483,7 @@ func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *prepar
registrations := make([]policy.ExtensionRegistration, 0, len(batch.extensions))
for _, extension := range batch.extensions {
if err := os.Rename(extension.stagingPath, extension.finalPath); err != nil {
- return false, nil, "", fmt.Errorf("commit extension directory %s: %w", extension.name, err)
+ return nil, "", fmt.Errorf("commit extension directory %s: %w", extension.name, err)
}
transaction.paths = append(transaction.paths, extension.finalPath)
registrations = append(registrations, policy.ExtensionRegistration{
@@ -488,7 +494,7 @@ func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *prepar
}
if err := s.policy.AddExtensions(registrations); err != nil {
- return false, nil, "", fmt.Errorf("failed to update enterprise policy: %w", err)
+ return nil, "", fmt.Errorf("failed to update enterprise policy: %w", err)
}
var newTokens []string
@@ -496,7 +502,7 @@ func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *prepar
newTokens = []string{fmt.Sprintf("--load-extension=%s", strings.Join(batch.flagPaths, ","))}
}
if _, err := s.mergeAndWriteChromiumFlags(ctx, newTokens); err != nil {
- return false, nil, "", err
+ return nil, "", err
}
committed = true
@@ -506,7 +512,7 @@ func (s *ApiService) commitPreparedExtensions(ctx context.Context, batch *prepar
"chromeExtensionID", extension.chromeExtensionID,
"requiresEnterprisePolicy", extension.requiresEnterprisePolicy)
}
- return batch.requiresRestart, transaction, "", nil
+ return transaction, "", nil
}
func (s *ApiService) loadUnpackedExtensions(ctx context.Context, extensions []preparedExtension) error {
diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go
index 98927e86..18d43c2b 100644
--- a/server/cmd/api/api/chromium_configure.go
+++ b/server/cmd/api/api/chromium_configure.go
@@ -53,6 +53,11 @@ func (st *chromiumConfigureState) cleanup() {
func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.ChromiumConfigureRequestObject) (oapi.ChromiumConfigureResponseObject, error) {
start := time.Now()
+ extensionLoadStrategy, strategyErr := chromiumConfigureExtensionLoadStrategy(request.Params)
+ if strategyErr != "" {
+ return cfg400(strategyErr), nil
+ }
+
if request.Body == nil {
return cfg400("request body required"), nil
}
@@ -77,15 +82,36 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu
return cfg400("no configuration fields provided"), nil
}
+ mode := chromiumConfigureModeFor(st, extensionLoadStrategy)
+ var preparedExtensions *preparedExtensionBatch
+ if mode == chromiumConfigureModePreferCDPExtensions {
+ var reqMsg string
+ var err error
+ preparedExtensions, reqMsg, err = s.prepareExtensionZipItems(ctx, st.extItems)
+ if preparedExtensions != nil {
+ defer preparedExtensions.cleanup()
+ }
+ if reqMsg != "" {
+ return cfg400(fmt.Sprintf("%s: %s", chromiumConfigureStepExtensions, reqMsg)), nil
+ }
+ if err != nil {
+ return cfg500ConfigureStep(chromiumConfigureStepExtensions, err.Error()), nil
+ }
+ }
+
s.chromiumConfigMu.Lock()
defer s.chromiumConfigMu.Unlock()
var configureResp oapi.ChromiumConfigureResponseObject
- switch chromiumConfigureModeFor(st) {
+ switch mode {
case chromiumConfigureModeLive:
configureResp = s.chromiumConfigureLive(ctx, st, spec)
case chromiumConfigureModeRestart:
- configureResp = s.chromiumConfigureRestart(ctx, st, spec)
+ configureResp = s.chromiumConfigureRestart(ctx, st, spec, func() (string, error) {
+ return s.installExtensionZipItems(ctx, st.extItems)
+ }, nil)
+ case chromiumConfigureModePreferCDPExtensions:
+ configureResp = s.chromiumConfigurePreferCDPExtensions(ctx, st, spec, preparedExtensions)
default:
return cfg500Configure("unhandled configure mode"), nil
}
@@ -102,15 +128,31 @@ type chromiumConfigureMode uint8
const (
chromiumConfigureModeLive chromiumConfigureMode = iota
chromiumConfigureModeRestart
+ chromiumConfigureModePreferCDPExtensions
)
-func chromiumConfigureModeFor(st *chromiumConfigureState) chromiumConfigureMode {
+func chromiumConfigureExtensionLoadStrategy(params oapi.ChromiumConfigureParams) (oapi.ChromiumConfigureParamsExtensionLoadStrategy, string) {
+ if params.ExtensionLoadStrategy == nil {
+ return oapi.Restart, ""
+ }
+ if !params.ExtensionLoadStrategy.Valid() {
+ return "", "extension_load_strategy must be restart or prefer_cdp"
+ }
+ return *params.ExtensionLoadStrategy, ""
+}
+
+func chromiumConfigureModeFor(st *chromiumConfigureState, strategy oapi.ChromiumConfigureParamsExtensionLoadStrategy) chromiumConfigureMode {
if st.hasProfile ||
- len(st.extItems) > 0 ||
policiesContentNonEmpty(st.chromePoliciesJSON) ||
flagsContentNonEmpty(st.chromiumFlagsJSON) {
return chromiumConfigureModeRestart
}
+ if len(st.extItems) > 0 {
+ if strategy == oapi.PreferCdp {
+ return chromiumConfigureModePreferCDPExtensions
+ }
+ return chromiumConfigureModeRestart
+ }
return chromiumConfigureModeLive
}
@@ -140,7 +182,50 @@ func chromiumConfigureNavigate(ctx context.Context, s *ApiService, spec startURL
}
}
-func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumConfigureState, spec startURLParsed) (resp oapi.ChromiumConfigureResponseObject) {
+func (s *ApiService) chromiumConfigurePreferCDPExtensions(ctx context.Context, st *chromiumConfigureState, spec startURLParsed, prepared *preparedExtensionBatch) oapi.ChromiumConfigureResponseObject {
+ commitExtensions := func() (string, error) {
+ _, reqMsg, err := s.commitPreparedExtensions(ctx, prepared)
+ return reqMsg, err
+ }
+ if prepared.requiresRestart {
+ return s.chromiumConfigureRestart(ctx, st, spec, commitExtensions, func() oapi.ChromiumConfigureResponseObject {
+ if err := s.verifyUnpackedExtensions(ctx, prepared.extensions); err != nil {
+ return cfg500ConfigureStep(chromiumConfigureStepExtensions, err.Error())
+ }
+ return nil
+ })
+ }
+
+ // Configure keeps the default restart path's non-transactional install semantics.
+ // Rolling back after fallback verification would require a second restart.
+ reqMsg, err := commitExtensions()
+ if reqMsg != "" {
+ return cfg400(fmt.Sprintf("%s: %s", chromiumConfigureStepExtensions, reqMsg))
+ }
+ if err != nil {
+ return cfg500ConfigureStep(chromiumConfigureStepExtensions, err.Error())
+ }
+
+ loadErr := s.loadUnpackedExtensions(ctx, prepared.extensions)
+ if loadErr == nil {
+ return s.chromiumConfigureLive(ctx, st, spec)
+ }
+ logger.FromContext(ctx).Warn("CDP extension load failed during configure, restarting Chromium", "error", loadErr)
+
+ // Extensions are already persisted. The fallback re-launches Chromium with
+ // the merged flags and still applies any pending display change.
+ return s.chromiumConfigureRestart(ctx, st, spec, nil, func() oapi.ChromiumConfigureResponseObject {
+ if err := s.verifyUnpackedExtensions(ctx, prepared.extensions); err != nil {
+ return cfg500ConfigureStep(chromiumConfigureStepExtensions, errors.Join(loadErr, err).Error())
+ }
+ return nil
+ })
+}
+
+type chromiumConfigureExtensionInstaller func() (string, error)
+type chromiumConfigurePostRestartCheck func() oapi.ChromiumConfigureResponseObject
+
+func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumConfigureState, spec startURLParsed, installExtensions chromiumConfigureExtensionInstaller, postRestartCheck chromiumConfigurePostRestartCheck) (resp oapi.ChromiumConfigureResponseObject) {
var stoppedRecordings []stoppedRecordingInfo
chromiumStopped := false
restartAfterStop := func() error {
@@ -183,10 +268,14 @@ func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumC
return cfgResponseFromStepError(chromiumConfigureStepPolicies, err)
}
- if reqMsgs, ierr := chromiumApplyExtensions(ctx, s, st.extItems); reqMsgs != "" {
- return cfg400(fmt.Sprintf("%s: %s", chromiumConfigureStepExtensions, reqMsgs))
- } else if ierr != nil {
- return cfg500ConfigureStep(chromiumConfigureStepExtensions, ierr.Error())
+ if installExtensions != nil {
+ extensionReqMsg, extensionErr := installExtensions()
+ if extensionReqMsg != "" {
+ return cfg400(fmt.Sprintf("%s: %s", chromiumConfigureStepExtensions, extensionReqMsg))
+ }
+ if extensionErr != nil {
+ return cfg500ConfigureStep(chromiumConfigureStepExtensions, extensionErr.Error())
+ }
}
if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" {
@@ -235,6 +324,11 @@ func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumC
if err := restartAfterStop(); err != nil {
return cfg500ConfigureStep(chromiumConfigureStepStart, err.Error())
}
+ if postRestartCheck != nil {
+ if resp := postRestartCheck(); resp != nil {
+ return resp
+ }
+ }
chromiumConfigureNavigate(ctx, s, spec)
if len(stoppedRecordings) > 0 {
go s.startNewRecordingSegments(context.WithoutCancel(ctx), stoppedRecordings)
@@ -804,14 +898,6 @@ func chromiumApplyPolicies(ctx context.Context, s *ApiService, overrides policy.
return nil
}
-func chromiumApplyExtensions(ctx context.Context, s *ApiService, items []extensionZipItem) (string, error) {
- if len(items) == 0 {
- return "", nil
- }
- _, reqMsg, err := s.applyExtensionZipItems(ctx, items)
- return reqMsg, err
-}
-
func chromiumValidateFlags(raw *string) (*chromiumFlagsPlan, error) {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil, nil
diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go
index f1b46c50..607acafa 100644
--- a/server/cmd/api/api/chromium_configure_test.go
+++ b/server/cmd/api/api/chromium_configure_test.go
@@ -10,6 +10,7 @@ import (
"strings"
"testing"
+ oapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/stretchr/testify/require"
)
@@ -33,31 +34,69 @@ func TestChromiumConfigureModeFor(t *testing.T) {
stringPtr := func(value string) *string { return &value }
tests := []struct {
- name string
- state chromiumConfigureState
- want chromiumConfigureMode
+ name string
+ state chromiumConfigureState
+ strategy oapi.ChromiumConfigureParamsExtensionLoadStrategy
+ want chromiumConfigureMode
}{
- {name: "no restart fields", want: chromiumConfigureModeLive},
- {name: "display only", state: chromiumConfigureState{displayJSON: stringPtr(`{"width":1280}`)}, want: chromiumConfigureModeLive},
- {name: "start URL only", state: chromiumConfigureState{startURLRaw: stringPtr("https://example.com")}, want: chromiumConfigureModeLive},
- {name: "empty policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{}`)}, want: chromiumConfigureModeLive},
- {name: "nonempty policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{"QuicAllowed":false}`)}, want: chromiumConfigureModeRestart},
- {name: "invalid policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{bad-json`)}, want: chromiumConfigureModeRestart},
- {name: "empty flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{"flags":[]}`)}, want: chromiumConfigureModeLive},
- {name: "nonempty flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{"flags":["--kiosk"]}`)}, want: chromiumConfigureModeRestart},
- {name: "invalid flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{bad-json`)}, want: chromiumConfigureModeRestart},
- {name: "profile", state: chromiumConfigureState{hasProfile: true}, want: chromiumConfigureModeRestart},
- {name: "extensions", state: chromiumConfigureState{extItems: []extensionZipItem{{name: "test"}}}, want: chromiumConfigureModeRestart},
- {name: "display and extension", state: chromiumConfigureState{displayJSON: stringPtr(`{"width":1280}`), extItems: []extensionZipItem{{name: "test"}}}, want: chromiumConfigureModeRestart},
+ {name: "no restart fields", strategy: oapi.Restart, want: chromiumConfigureModeLive},
+ {name: "display only", state: chromiumConfigureState{displayJSON: stringPtr(`{"width":1280}`)}, strategy: oapi.Restart, want: chromiumConfigureModeLive},
+ {name: "start URL only", state: chromiumConfigureState{startURLRaw: stringPtr("https://example.com")}, strategy: oapi.Restart, want: chromiumConfigureModeLive},
+ {name: "empty policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{}`)}, strategy: oapi.Restart, want: chromiumConfigureModeLive},
+ {name: "nonempty policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{"QuicAllowed":false}`)}, strategy: oapi.PreferCdp, want: chromiumConfigureModeRestart},
+ {name: "invalid policies", state: chromiumConfigureState{chromePoliciesJSON: stringPtr(`{bad-json`)}, strategy: oapi.PreferCdp, want: chromiumConfigureModeRestart},
+ {name: "empty flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{"flags":[]}`)}, strategy: oapi.Restart, want: chromiumConfigureModeLive},
+ {name: "nonempty flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{"flags":["--kiosk"]}`)}, strategy: oapi.PreferCdp, want: chromiumConfigureModeRestart},
+ {name: "invalid flags", state: chromiumConfigureState{chromiumFlagsJSON: stringPtr(`{bad-json`)}, strategy: oapi.PreferCdp, want: chromiumConfigureModeRestart},
+ {name: "profile", state: chromiumConfigureState{hasProfile: true}, strategy: oapi.PreferCdp, want: chromiumConfigureModeRestart},
+ {name: "extensions default restart", state: chromiumConfigureState{extItems: []extensionZipItem{{name: "test"}}}, strategy: oapi.Restart, want: chromiumConfigureModeRestart},
+ {name: "extensions prefer CDP", state: chromiumConfigureState{extItems: []extensionZipItem{{name: "test"}}}, strategy: oapi.PreferCdp, want: chromiumConfigureModePreferCDPExtensions},
+ {name: "display and extensions prefer CDP", state: chromiumConfigureState{displayJSON: stringPtr(`{"width":1280}`), extItems: []extensionZipItem{{name: "test"}}}, strategy: oapi.PreferCdp, want: chromiumConfigureModePreferCDPExtensions},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- require.Equal(t, tt.want, chromiumConfigureModeFor(&tt.state))
+ require.Equal(t, tt.want, chromiumConfigureModeFor(&tt.state, tt.strategy))
})
}
}
+func TestChromiumConfigureExtensionLoadStrategy(t *testing.T) {
+ strategy, msg := chromiumConfigureExtensionLoadStrategy(oapi.ChromiumConfigureParams{})
+ require.Empty(t, msg)
+ require.Equal(t, oapi.Restart, strategy)
+
+ preferCDP := oapi.PreferCdp
+ strategy, msg = chromiumConfigureExtensionLoadStrategy(oapi.ChromiumConfigureParams{ExtensionLoadStrategy: &preferCDP})
+ require.Empty(t, msg)
+ require.Equal(t, oapi.PreferCdp, strategy)
+
+ invalid := oapi.ChromiumConfigureParamsExtensionLoadStrategy("invalid")
+ _, msg = chromiumConfigureExtensionLoadStrategy(oapi.ChromiumConfigureParams{ExtensionLoadStrategy: &invalid})
+ require.Equal(t, "extension_load_strategy must be restart or prefer_cdp", msg)
+}
+
+func TestChromiumConfigureGeneratedClientQuery(t *testing.T) {
+ preferCDP := oapi.PreferCdp
+ req, err := oapi.NewChromiumConfigureRequestWithBody(
+ "http://example.test",
+ &oapi.ChromiumConfigureParams{ExtensionLoadStrategy: &preferCDP},
+ "multipart/form-data; boundary=test",
+ strings.NewReader("--test--"),
+ )
+ require.NoError(t, err)
+ require.Equal(t, "prefer_cdp", req.URL.Query().Get("extension_load_strategy"))
+
+ req, err = oapi.NewChromiumConfigureRequestWithBody(
+ "http://example.test",
+ nil,
+ "multipart/form-data; boundary=test",
+ strings.NewReader("--test--"),
+ )
+ require.NoError(t, err)
+ require.Empty(t, req.URL.RawQuery)
+}
+
func TestChromiumConfigureActionables(t *testing.T) {
emptyFlags := `{"flags":[]}`
realFlags := `{"flags":["--kiosk"]}`
diff --git a/server/e2e/e2e_chromium_configure_powerset_test.go b/server/e2e/e2e_chromium_configure_powerset_test.go
index 0342da87..c1073573 100644
--- a/server/e2e/e2e_chromium_configure_powerset_test.go
+++ b/server/e2e/e2e_chromium_configure_powerset_test.go
@@ -86,7 +86,7 @@ func TestChromiumConfigureMultipartPowerset(t *testing.T) {
client, err := c.APIClient()
require.NoError(t, err)
- rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(bytes.NewReader(body.Bytes())))
+ rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, nil, w.FormDataContentType(), io.NopCloser(bytes.NewReader(body.Bytes())))
require.NoError(t, err)
require.Equal(t, http.StatusOK, rsp.StatusCode(),
@@ -217,7 +217,7 @@ func TestChromiumConfigureStartURLBareHost(t *testing.T) {
client, err := c.APIClient()
require.NoError(t, err)
- rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, mw.FormDataContentType(), io.NopCloser(bytes.NewReader(buf.Bytes())))
+ rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, nil, mw.FormDataContentType(), io.NopCloser(bytes.NewReader(buf.Bytes())))
require.NoError(t, err)
require.Equal(t, http.StatusOK, rsp.StatusCode(), "%s", string(rsp.Body))
require.True(t, rsp.JSON200.Ok)
diff --git a/server/e2e/e2e_chromium_configure_test.go b/server/e2e/e2e_chromium_configure_test.go
index 282771a5..985d1021 100644
--- a/server/e2e/e2e_chromium_configure_test.go
+++ b/server/e2e/e2e_chromium_configure_test.go
@@ -6,14 +6,284 @@ import (
"io"
"mime/multipart"
"net/http"
+ "os"
"os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
"testing"
"time"
+ "github.com/kernel/kernel-images/server/lib/cdpclient"
instanceoapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/stretchr/testify/require"
)
+type configureExtensionPart struct {
+ name string
+ zip []byte
+}
+
+type configureE2ERequest struct {
+ params *instanceoapi.ChromiumConfigureParams
+ extensions []configureExtensionPart
+ display string
+ chromiumFlags string
+ startURL string
+}
+
+func chromiumConfigureE2E(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses, request configureE2ERequest) *instanceoapi.ChromiumConfigureResponse {
+ t.Helper()
+
+ var body bytes.Buffer
+ writer := multipart.NewWriter(&body)
+ for _, extension := range request.extensions {
+ part, err := writer.CreateFormFile("extensions.zip_file", extension.name+".zip")
+ require.NoError(t, err)
+ _, err = part.Write(extension.zip)
+ require.NoError(t, err)
+ require.NoError(t, writer.WriteField("extensions.name", extension.name))
+ }
+ if request.display != "" {
+ require.NoError(t, writer.WriteField("display", request.display))
+ }
+ if request.chromiumFlags != "" {
+ require.NoError(t, writer.WriteField("chromium_flags", request.chromiumFlags))
+ }
+ if request.startURL != "" {
+ require.NoError(t, writer.WriteField("start_url", request.startURL))
+ }
+ require.NoError(t, writer.Close())
+
+ response, err := client.ChromiumConfigureWithBodyWithResponse(ctx, request.params, writer.FormDataContentType(), bytes.NewReader(body.Bytes()))
+ require.NoError(t, err)
+ return response
+}
+
+func chromiumConfigureStartCount(t *testing.T, ctx context.Context, c *TestContainer) int {
+ t.Helper()
+ output, err := execCombinedOutputWithClient(ctx, c, "sh", []string{"-lc", `grep -c 'starting chromium via supervisorctl' /var/log/supervisord/kernel-images-api || true`})
+ require.NoError(t, err)
+ count, err := strconv.Atoi(strings.TrimSpace(output))
+ require.NoError(t, err)
+ return count
+}
+
+func requireConfiguredExtensionActive(t *testing.T, ctx context.Context, c *TestContainer, name string) {
+ t.Helper()
+ path := filepath.Join("/home/kernel/extensions", name)
+ require.Eventually(t, func() bool {
+ client, err := cdpclient.Dial(ctx, c.CDPURL())
+ if err != nil {
+ return false
+ }
+ defer client.Close()
+ extensions, err := client.GetExtensions(ctx)
+ if err != nil {
+ return false
+ }
+ for _, extension := range extensions {
+ if extension.Enabled && filepath.Clean(extension.Path) == path {
+ return true
+ }
+ }
+ return false
+ }, 10*time.Second, 200*time.Millisecond)
+}
+
+func TestChromiumConfigureExtensionLoadStrategies(t *testing.T) {
+ if _, err := exec.LookPath("docker"); err != nil {
+ t.Skipf("docker not available: %v", err)
+ }
+
+ for _, test := range []struct {
+ name string
+ image string
+ testPartialFailure bool
+ }{
+ {name: "headless", image: headlessImage, testPartialFailure: true},
+ {name: "headful", image: headfulImage},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+ testChromiumConfigureExtensionLoadStrategies(t, test.image, test.testPartialFailure)
+ })
+ }
+}
+
+func testChromiumConfigureExtensionLoadStrategies(t *testing.T, image string, testPartialFailure bool) {
+ ordinaryDir, err := filepath.Abs("test-extension")
+ require.NoError(t, err)
+ ordinaryZip, err := zipDirToBytes(ordinaryDir)
+ require.NoError(t, err)
+ enterpriseDir, err := filepath.Abs("test-extension-enterprise")
+ require.NoError(t, err)
+ enterpriseZip, err := zipDirToBytes(enterpriseDir)
+ require.NoError(t, err)
+ invalidDir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(invalidDir, "manifest.json"), []byte(`{
+ "manifest_version": 3,
+ "name": "Invalid Configure Extension"
+ }`), 0o600))
+ invalidZip, err := zipDirToBytes(invalidDir)
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+ c := NewTestContainer(t, image)
+ require.NoError(t, c.Start(ctx, ContainerConfig{
+ Env: map[string]string{"WIDTH": "1024", "HEIGHT": "768"},
+ }))
+ defer c.Stop(context.WithoutCancel(ctx))
+ require.NoError(t, c.WaitReady(ctx))
+ require.NoError(t, c.WaitDevTools(ctx))
+ client, err := c.APIClient()
+ require.NoError(t, err)
+
+ preferCDP := instanceoapi.PreferCdp
+ preferCDPParams := &instanceoapi.ChromiumConfigureParams{ExtensionLoadStrategy: &preferCDP}
+
+ t.Run("default restart", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ extensions: []configureExtensionPart{{name: "configure-default", zip: ordinaryZip}},
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.NotEqual(t, before, after)
+ require.Equal(t, starts+1, chromiumConfigureStartCount(t, ctx, c))
+ requireConfiguredExtensionActive(t, ctx, c, "configure-default")
+ })
+
+ t.Run("prefer CDP", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{{name: "configure-cdp", zip: ordinaryZip}},
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.Equal(t, before, after)
+ requireConfiguredExtensionActive(t, ctx, c, "configure-cdp")
+ })
+
+ t.Run("prefer CDP with display", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{{name: "configure-cdp-display", zip: ordinaryZip}},
+ display: `{"width":1280,"height":720,"refresh_rate":60,"restart_chromium":false,"require_idle":true}`,
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.Equal(t, before, after)
+ require.Equal(t, starts, chromiumConfigureStartCount(t, ctx, c))
+ requireConfiguredExtensionActive(t, ctx, c, "configure-cdp-display")
+ waitForXRootResolution(t, ctx, c, 1280, 720, 10*time.Second)
+ })
+
+ t.Run("startup field restart", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{{name: "configure-startup", zip: ordinaryZip}},
+ chromiumFlags: `{"flags":["--disable-background-networking"]}`,
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.NotEqual(t, before, after)
+ require.Equal(t, starts+1, chromiumConfigureStartCount(t, ctx, c))
+ requireConfiguredExtensionActive(t, ctx, c, "configure-startup")
+ })
+
+ t.Run("enterprise restart", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{
+ {name: "configure-enterprise", zip: enterpriseZip},
+ {name: "configure-enterprise-unpacked", zip: ordinaryZip},
+ },
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.NotEqual(t, before, after)
+ require.Equal(t, starts+1, chromiumConfigureStartCount(t, ctx, c))
+ _, err = execCombinedOutputWithClient(ctx, c, "grep", []string{"-q", "configure-enterprise", "/etc/chromium/policies/managed/policy.json"})
+ require.NoError(t, err)
+ requireConfiguredExtensionActive(t, ctx, c, "configure-enterprise-unpacked")
+ })
+
+ t.Run("CDP failure restart", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ _, err = execCombinedOutputWithClient(ctx, c, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "stop", "chromium"})
+ require.NoError(t, err)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{
+ {name: "configure-fallback-one", zip: ordinaryZip},
+ {name: "configure-fallback-two", zip: ordinaryZip},
+ },
+ })
+ require.Equal(t, http.StatusOK, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.NotEqual(t, before, after)
+ require.Equal(t, starts+1, chromiumConfigureStartCount(t, ctx, c))
+ requireConfiguredExtensionActive(t, ctx, c, "configure-fallback-one")
+ requireConfiguredExtensionActive(t, ctx, c, "configure-fallback-two")
+ })
+
+ if testPartialFailure {
+ // The malformed fixture prevents headful Chromium from becoming ready after restart.
+ t.Run("partial CDP failure restart", func(t *testing.T) {
+ before, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ starts := chromiumConfigureStartCount(t, ctx, c)
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: preferCDPParams,
+ extensions: []configureExtensionPart{
+ {name: "configure-partial-one", zip: ordinaryZip},
+ {name: "configure-partial-invalid", zip: invalidZip},
+ },
+ })
+ require.Equal(t, http.StatusInternalServerError, response.StatusCode(), "%s", response.Body)
+ after, err := fetchBrowserWebSocketURL(ctx, c)
+ require.NoError(t, err)
+ require.NotEqual(t, before, after)
+ require.Equal(t, starts+1, chromiumConfigureStartCount(t, ctx, c))
+ requireConfiguredExtensionActive(t, ctx, c, "configure-partial-one")
+ _, err = execCombinedOutputWithClient(ctx, c, "grep", []string{"-q", "loaded unpacked extension over CDP.*name=configure-partial-one", "/var/log/supervisord/kernel-images-api"})
+ require.NoError(t, err)
+ })
+ }
+
+ t.Run("invalid strategy", func(t *testing.T) {
+ invalid := instanceoapi.ChromiumConfigureParamsExtensionLoadStrategy("invalid")
+ response := chromiumConfigureE2E(t, ctx, client, configureE2ERequest{
+ params: &instanceoapi.ChromiumConfigureParams{ExtensionLoadStrategy: &invalid},
+ startURL: "data:text/html,
invalid-strategy",
+ })
+ require.Equal(t, http.StatusBadRequest, response.StatusCode(), "%s", response.Body)
+ })
+}
+
func TestChromiumConfigureStartURLBare(t *testing.T) {
t.Parallel()
@@ -44,7 +314,7 @@ func TestChromiumConfigureStartURLBare(t *testing.T) {
require.NoError(t, w.WriteField("start_url", startURL))
require.NoError(t, w.Close())
- rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(&buf))
+ rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, nil, w.FormDataContentType(), io.NopCloser(&buf))
require.NoError(t, err)
require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status=%s body=%s", rsp.Status(), string(rsp.Body))
diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go
index 7610a6db..502db1ca 100644
--- a/server/lib/oapi/oapi.go
+++ b/server/lib/oapi/oapi.go
@@ -2903,6 +2903,24 @@ func (e TelemetryEventCategory) Valid() bool {
}
}
+// Defines values for ChromiumConfigureParamsExtensionLoadStrategy.
+const (
+ PreferCdp ChromiumConfigureParamsExtensionLoadStrategy = "prefer_cdp"
+ Restart ChromiumConfigureParamsExtensionLoadStrategy = "restart"
+)
+
+// Valid indicates whether the value is a known member of the ChromiumConfigureParamsExtensionLoadStrategy enum.
+func (e ChromiumConfigureParamsExtensionLoadStrategy) Valid() bool {
+ switch e {
+ case PreferCdp:
+ return true
+ case Restart:
+ return true
+ default:
+ return false
+ }
+}
+
// Defines values for DownloadDirZstdParamsCompressionLevel.
const (
Best DownloadDirZstdParamsCompressionLevel = "best"
@@ -6695,6 +6713,15 @@ type ChromiumConfigureMultipartBody struct {
StripComponents *string `json:"strip_components,omitempty"`
}
+// ChromiumConfigureParams defines parameters for ChromiumConfigure.
+type ChromiumConfigureParams struct {
+ // ExtensionLoadStrategy Extension activation strategy. Omit or use `restart` to preserve unconditional restart behavior for requests containing extensions. `prefer_cdp` attempts live activation for eligible unpacked extensions and falls back to one restart when required.
+ ExtensionLoadStrategy *ChromiumConfigureParamsExtensionLoadStrategy `form:"extension_load_strategy,omitempty" json:"extension_load_strategy,omitempty"`
+}
+
+// ChromiumConfigureParamsExtensionLoadStrategy defines parameters for ChromiumConfigure.
+type ChromiumConfigureParamsExtensionLoadStrategy string
+
// DownloadDirZipParams defines parameters for DownloadDirZip.
type DownloadDirZipParams struct {
// Path Absolute directory path to archive and download.
@@ -9343,7 +9370,7 @@ type ClientInterface interface {
TypeText(ctx context.Context, body TypeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// ChromiumConfigureWithBody request with any body
- ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+ ChromiumConfigureWithBody(ctx context.Context, params *ChromiumConfigureParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
// PatchDisplayWithBody request with any body
PatchDisplayWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -9842,8 +9869,8 @@ func (c *Client) TypeText(ctx context.Context, body TypeTextJSONRequestBody, req
return c.Client.Do(req)
}
-func (c *Client) ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
- req, err := NewChromiumConfigureRequestWithBody(c.Server, contentType, body)
+func (c *Client) ChromiumConfigureWithBody(ctx context.Context, params *ChromiumConfigureParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewChromiumConfigureRequestWithBody(c.Server, params, contentType, body)
if err != nil {
return nil, err
}
@@ -11167,7 +11194,7 @@ func NewTypeTextRequestWithBody(server string, contentType string, body io.Reade
}
// NewChromiumConfigureRequestWithBody generates requests for ChromiumConfigure with any type of body
-func NewChromiumConfigureRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
+func NewChromiumConfigureRequestWithBody(server string, params *ChromiumConfigureParams, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
@@ -11185,6 +11212,28 @@ func NewChromiumConfigureRequestWithBody(server string, contentType string, body
return nil, err
}
+ if params != nil {
+ queryValues := queryURL.Query()
+
+ if params.ExtensionLoadStrategy != nil {
+
+ if queryFrag, err := runtime.StyleParamWithOptions("form", true, "extension_load_strategy", *params.ExtensionLoadStrategy, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ queryURL.RawQuery = queryValues.Encode()
+ }
+
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
@@ -12973,7 +13022,7 @@ type ClientWithResponsesInterface interface {
TypeTextWithResponse(ctx context.Context, body TypeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*TypeTextResponse, error)
// ChromiumConfigureWithBodyWithResponse request with any body
- ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error)
+ ChromiumConfigureWithBodyWithResponse(ctx context.Context, params *ChromiumConfigureParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error)
// PatchDisplayWithBodyWithResponse request with any body
PatchDisplayWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchDisplayResponse, error)
@@ -14732,8 +14781,8 @@ func (c *ClientWithResponses) TypeTextWithResponse(ctx context.Context, body Typ
}
// ChromiumConfigureWithBodyWithResponse request with arbitrary body returning *ChromiumConfigureResponse
-func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) {
- rsp, err := c.ChromiumConfigureWithBody(ctx, contentType, body, reqEditors...)
+func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, params *ChromiumConfigureParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) {
+ rsp, err := c.ChromiumConfigureWithBody(ctx, params, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
@@ -17477,7 +17526,7 @@ type ServerInterface interface {
TypeText(w http.ResponseWriter, r *http.Request)
// Apply batched session and browser configuration plus optional navigation
// (POST /configure)
- ChromiumConfigure(w http.ResponseWriter, r *http.Request)
+ ChromiumConfigure(w http.ResponseWriter, r *http.Request, params ChromiumConfigureParams)
// Update display configuration
// (PATCH /display)
PatchDisplay(w http.ResponseWriter, r *http.Request)
@@ -17702,7 +17751,7 @@ func (_ Unimplemented) TypeText(w http.ResponseWriter, r *http.Request) {
// Apply batched session and browser configuration plus optional navigation
// (POST /configure)
-func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request) {
+func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request, params ChromiumConfigureParams) {
w.WriteHeader(http.StatusNotImplemented)
}
@@ -18182,8 +18231,21 @@ func (siw *ServerInterfaceWrapper) TypeText(w http.ResponseWriter, r *http.Reque
// ChromiumConfigure operation middleware
func (siw *ServerInterfaceWrapper) ChromiumConfigure(w http.ResponseWriter, r *http.Request) {
+ var err error
+
+ // Parameter object where we will unmarshal all parameters from the context
+ var params ChromiumConfigureParams
+
+ // ------------- Optional query parameter "extension_load_strategy" -------------
+
+ err = runtime.BindQueryParameterWithOptions("form", true, false, "extension_load_strategy", r.URL.Query(), ¶ms.ExtensionLoadStrategy, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ if err != nil {
+ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "extension_load_strategy", Err: err})
+ return
+ }
+
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- siw.Handler.ChromiumConfigure(w, r)
+ siw.Handler.ChromiumConfigure(w, r, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
@@ -19892,7 +19954,8 @@ func (response TypeText500JSONResponse) VisitTypeTextResponse(w http.ResponseWri
}
type ChromiumConfigureRequestObject struct {
- Body *multipart.Reader
+ Params ChromiumConfigureParams
+ Body *multipart.Reader
}
type ChromiumConfigureResponseObject interface {
@@ -22432,9 +22495,11 @@ func (sh *strictHandler) TypeText(w http.ResponseWriter, r *http.Request) {
}
// ChromiumConfigure operation middleware
-func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request) {
+func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request, params ChromiumConfigureParams) {
var request ChromiumConfigureRequestObject
+ request.Params = params
+
if reader, err := r.MultipartReader(); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err))
return
@@ -24032,170 +24097,174 @@ var swaggerSpec = []string{
"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",
- "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",
- "j8EDLF/gv3z5/wEAAP//OIBHLnhYAwA=",
+ "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",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/server/openapi.yaml b/server/openapi.yaml
index d58ba504..24d9743b 100644
--- a/server/openapi.yaml
+++ b/server/openapi.yaml
@@ -1260,19 +1260,35 @@ paths:
post:
summary: Apply batched session and browser configuration plus optional navigation
description: |
- Optional multipart parts apply configuration while Chromium stays stopped once (policy,
- flags, extensions, profile archive, optional display sizing), then Chromium is started exactly
- once and DevTools readiness is awaited. Optional `start_url` dispatches a best-effort
- navigation after readiness without waiting for page load. Bare hosts are normalized to
- `https://`. Omit any part you do not need. At least one actionable part must be present.
- Required configuration steps run in this order: policies, extensions, display, flags,
- then profile archive. Chromium is started only once at the end. The endpoint is not
- transactional: if a later required step fails, earlier successful side effects may remain
- on the instance while the non-2xx error propagates.
+ Optional multipart parts apply browser and session configuration in one serialized request.
+ Profiles, nonempty policies, nonempty flags, and extensions use one Chromium stop/apply/start
+ cycle by default. Display-only and start_url-only requests stay live. Set
+ `extension_load_strategy=prefer_cdp` to attempt live CDP activation when extensions are present
+ without a profile, nonempty policies, or nonempty flags. Enterprise-policy extensions and CDP
+ activation failures fall back to one restart within the same request. The query parameter is
+ ignored by older images, which retain the default restart behavior.
+ Optional `start_url` dispatches a best-effort navigation after configuration and readiness
+ without waiting for page load. Bare hosts are normalized to `https://`. Omit any part you do
+ not need. At least one actionable part must be present. Required configuration steps run in
+ this order: policies, extensions, display, flags, then profile archive. The endpoint is not
+ transactional: if a later required step fails, earlier successful side effects may remain on
+ the instance while the non-2xx error propagates.
Prefer this over separate `/chromium/*` and `/display` calls when multiple restart-triggering
steps apply in one session configure.
operationId: chromiumConfigure
x-telemetry-category: platform
+ parameters:
+ - name: extension_load_strategy
+ in: query
+ required: false
+ description: >-
+ Extension activation strategy. Omit or use `restart` to preserve unconditional restart
+ behavior for requests containing extensions. `prefer_cdp` attempts live activation for
+ eligible unpacked extensions and falls back to one restart when required.
+ schema:
+ type: string
+ enum: [restart, prefer_cdp]
+ default: restart
requestBody:
required: true
content: