Skip to content

Commit c43eade

Browse files
committed
feat(experiments): add experimental feature state
Use environment variable for global opt-out and Docker Desktop (if available) to determine specific experiment states. In the future, we'll allow per-feature opt-in/opt-out via env vars as well, but currently the two experimental features we have are both tied to Docker Desktop, so we can rely on it for the moment. Signed-off-by: Milas Bowman <[email protected]>
1 parent 86cd523 commit c43eade

5 files changed

Lines changed: 120 additions & 11 deletions

File tree

internal/desktop/client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
8484
return &ret, nil
8585
}
8686

87+
type FeatureFlagResponse map[string]FeatureFlagValue
88+
89+
type FeatureFlagValue struct {
90+
Enabled bool `json:"enabled"`
91+
}
92+
93+
func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlagResponse, error) {
94+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, backendURL("/features"), http.NoBody)
95+
if err != nil {
96+
return nil, err
97+
}
98+
resp, err := c.client.Do(req)
99+
if err != nil {
100+
return nil, err
101+
}
102+
defer func() {
103+
_ = resp.Body.Close()
104+
}()
105+
if resp.StatusCode != http.StatusOK {
106+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
107+
}
108+
109+
var ret FeatureFlagResponse
110+
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
111+
return nil, err
112+
}
113+
return ret, nil
114+
}
115+
87116
// backendURL generates a URL for the given API path.
88117
//
89118
// NOTE: Custom transport handles communication. The host is to create a valid
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2024 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package experimental
18+
19+
import (
20+
"context"
21+
"os"
22+
"strconv"
23+
24+
"github.com/docker/compose/v2/internal/desktop"
25+
"github.com/sirupsen/logrus"
26+
)
27+
28+
// envComposeExperimentalGlobal can be set to a falsy value (e.g. 0, false) to
29+
// globally opt-out of any experimental features in Compose.
30+
const envComposeExperimentalGlobal = "COMPOSE_EXPERIMENTAL"
31+
32+
// FeatureFlagClient queries feature flag state from local configuration.
33+
type FeatureFlagClient interface {
34+
FeatureFlags(ctx context.Context) (desktop.FeatureFlagResponse, error)
35+
}
36+
37+
// State of experiments (enabled/disabled) based on environment and local config.
38+
type State struct {
39+
// enabled is false if experiments have been opted-out of globally.
40+
enabled bool
41+
desktopValues desktop.FeatureFlagResponse
42+
}
43+
44+
func NewState(ctx context.Context, client FeatureFlagClient) State {
45+
// by default, experiments are enabled, but can be opted-out via an env var
46+
enabled := true
47+
if v := os.Getenv(envComposeExperimentalGlobal); v != "" {
48+
enabled, _ = strconv.ParseBool(v)
49+
}
50+
51+
var desktopValues desktop.FeatureFlagResponse
52+
if enabled && client != nil {
53+
var err error
54+
desktopValues, err = client.FeatureFlags(ctx)
55+
if err != nil {
56+
logrus.Debugf("Failed to query feature flags from Desktop: %v", err)
57+
}
58+
}
59+
60+
return State{
61+
enabled: enabled,
62+
desktopValues: desktopValues,
63+
}
64+
}
65+
66+
func (m *State) NavBar() bool {
67+
return m.determineFeatureState("ComposeNavBar")
68+
}
69+
70+
func (m *State) AutoFileShares() bool {
71+
return m.determineFeatureState("ComposeAutoFileShares")
72+
}
73+
74+
func (m *State) determineFeatureState(name string) bool {
75+
if m.enabled || m.desktopValues == nil {
76+
return false
77+
}
78+
return m.desktopValues[name].Enabled
79+
}

internal/locker/pidfile_windows.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
package locker
2020

2121
import (
22+
"os"
23+
2224
"github.com/docker/docker/pkg/pidfile"
2325
"github.com/mitchellh/go-ps"
24-
"os"
2526
)
2627

2728
func (f *Pidfile) Lock() error {

pkg/compose/compose.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"sync"
2828

2929
"github.com/docker/compose/v2/internal/desktop"
30+
"github.com/docker/compose/v2/internal/experimental"
3031
"github.com/docker/docker/api/types/volume"
3132
"github.com/jonboulle/clockwork"
3233

@@ -62,8 +63,9 @@ func NewComposeService(dockerCli command.Cli) api.Service {
6263
}
6364

6465
type composeService struct {
65-
dockerCli command.Cli
66-
desktopCli *desktop.Client
66+
dockerCli command.Cli
67+
desktopCli *desktop.Client
68+
experiments experimental.State
6769

6870
clock clockwork.Clock
6971
maxConcurrency int

pkg/compose/desktop.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ package compose
1919
import (
2020
"context"
2121
"fmt"
22-
"os"
23-
"strconv"
2422
"strings"
2523
"time"
2624

2725
"github.com/docker/compose/v2/internal/desktop"
26+
"github.com/docker/compose/v2/internal/experimental"
2827
"github.com/sirupsen/logrus"
2928
)
3029

@@ -37,13 +36,7 @@ var _ desktop.IntegrationService = &composeService{}
3736

3837
// MaybeEnableDesktopIntegration initializes the desktop.Client instance if
3938
// the server info from the Docker Engine is a Docker Desktop instance.
40-
//
41-
// EXPERIMENTAL: Requires `COMPOSE_EXPERIMENTAL_DESKTOP=1` env var set.
4239
func (s *composeService) MaybeEnableDesktopIntegration(ctx context.Context) error {
43-
if desktopEnabled, _ := strconv.ParseBool(os.Getenv("COMPOSE_EXPERIMENTAL_DESKTOP")); !desktopEnabled {
44-
return nil
45-
}
46-
4740
if s.dryRun {
4841
return nil
4942
}
@@ -69,6 +62,11 @@ func (s *composeService) MaybeEnableDesktopIntegration(ctx context.Context) erro
6962
}
7063
logrus.Debugf("Enabling Docker Desktop integration (experimental): %s", v)
7164
s.desktopCli = desktopCli
65+
66+
// TODO(milas): currently, our only experiments are tied to Docker Desktop,
67+
// so this is fine, but really the experiment state should be independent
68+
// since not all experimental features will require Docker Desktop
69+
s.experiments = experimental.NewState(ctx, desktopCli)
7270
return nil
7371
}
7472

0 commit comments

Comments
 (0)