Skip to content

Commit dd34f7a

Browse files
authored
include: add experimental support for Git resources (docker#10811)
Requires setting `COMPOSE_EXPERIMENTAL_GIT_REMOTE=1`. Signed-off-by: Nicolas De Loof <[email protected]>
1 parent caad727 commit dd34f7a

File tree

3 files changed

+240
-19
lines changed

3 files changed

+240
-19
lines changed

cmd/compose/compose.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/compose-spec/compose-go/dotenv"
3030
buildx "github.com/docker/buildx/util/progress"
3131
"github.com/docker/cli/cli/command"
32+
"github.com/docker/compose/v2/pkg/remote"
3233

3334
"github.com/compose-spec/compose-go/cli"
3435
"github.com/compose-spec/compose-go/types"
@@ -134,7 +135,25 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc) func(cmd *cobra.Command, ar
134135
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
135136
func (o *ProjectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
136137
return Adapt(func(ctx context.Context, args []string) error {
137-
project, err := o.ToProject(args, cli.WithResolvedPaths(true), cli.WithDiscardEnvFile)
138+
options := []cli.ProjectOptionsFn{
139+
cli.WithResolvedPaths(true),
140+
cli.WithDiscardEnvFile,
141+
cli.WithContext(ctx),
142+
}
143+
144+
enabled, err := remote.GitRemoteLoaderEnabled()
145+
if err != nil {
146+
return err
147+
}
148+
if enabled {
149+
git, err := remote.NewGitRemoteLoader()
150+
if err != nil {
151+
return err
152+
}
153+
options = append(options, cli.WithResourceLoader(git))
154+
}
155+
156+
project, err := o.ToProject(args, options...)
138157
if err != nil {
139158
return err
140159
}

cmd/compose/config.go

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/compose-spec/compose-go/cli"
2828
"github.com/compose-spec/compose-go/types"
29+
"github.com/docker/compose/v2/pkg/remote"
2930
"github.com/spf13/cobra"
3031

3132
"github.com/docker/compose/v2/pkg/api"
@@ -49,14 +50,21 @@ type configOptions struct {
4950
noConsistency bool
5051
}
5152

52-
func (o *configOptions) ToProject(services []string) (*types.Project, error) {
53+
func (o *configOptions) ToProject(ctx context.Context, services []string) (*types.Project, error) {
54+
git, err := remote.NewGitRemoteLoader()
55+
if err != nil {
56+
return nil, err
57+
}
58+
5359
return o.ProjectOptions.ToProject(services,
5460
cli.WithInterpolation(!o.noInterpolate),
5561
cli.WithResolvedPaths(!o.noResolvePath),
5662
cli.WithNormalization(!o.noNormalize),
5763
cli.WithConsistency(!o.noConsistency),
5864
cli.WithProfiles(o.Profiles),
59-
cli.WithDiscardEnvFile)
65+
cli.WithDiscardEnvFile,
66+
cli.WithContext(ctx),
67+
cli.WithResourceLoader(git))
6068
}
6169

6270
func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
@@ -82,19 +90,19 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
8290
}),
8391
RunE: Adapt(func(ctx context.Context, args []string) error {
8492
if opts.services {
85-
return runServices(streams, opts)
93+
return runServices(ctx, streams, opts)
8694
}
8795
if opts.volumes {
88-
return runVolumes(streams, opts)
96+
return runVolumes(ctx, streams, opts)
8997
}
9098
if opts.hash != "" {
91-
return runHash(streams, opts)
99+
return runHash(ctx, streams, opts)
92100
}
93101
if opts.profiles {
94-
return runProfiles(streams, opts, args)
102+
return runProfiles(ctx, streams, opts, args)
95103
}
96104
if opts.images {
97-
return runConfigImages(streams, opts, args)
105+
return runConfigImages(ctx, streams, opts, args)
98106
}
99107

100108
return runConfig(ctx, streams, backend, opts, args)
@@ -122,7 +130,7 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
122130

123131
func runConfig(ctx context.Context, streams api.Streams, backend api.Service, opts configOptions, services []string) error {
124132
var content []byte
125-
project, err := opts.ToProject(services)
133+
project, err := opts.ToProject(ctx, services)
126134
if err != nil {
127135
return err
128136
}
@@ -151,8 +159,8 @@ func runConfig(ctx context.Context, streams api.Streams, backend api.Service, op
151159
return err
152160
}
153161

154-
func runServices(streams api.Streams, opts configOptions) error {
155-
project, err := opts.ToProject(nil)
162+
func runServices(ctx context.Context, streams api.Streams, opts configOptions) error {
163+
project, err := opts.ToProject(ctx, nil)
156164
if err != nil {
157165
return err
158166
}
@@ -162,8 +170,8 @@ func runServices(streams api.Streams, opts configOptions) error {
162170
})
163171
}
164172

165-
func runVolumes(streams api.Streams, opts configOptions) error {
166-
project, err := opts.ToProject(nil)
173+
func runVolumes(ctx context.Context, streams api.Streams, opts configOptions) error {
174+
project, err := opts.ToProject(ctx, nil)
167175
if err != nil {
168176
return err
169177
}
@@ -173,12 +181,12 @@ func runVolumes(streams api.Streams, opts configOptions) error {
173181
return nil
174182
}
175183

176-
func runHash(streams api.Streams, opts configOptions) error {
184+
func runHash(ctx context.Context, streams api.Streams, opts configOptions) error {
177185
var services []string
178186
if opts.hash != "*" {
179187
services = append(services, strings.Split(opts.hash, ",")...)
180188
}
181-
project, err := opts.ToProject(nil)
189+
project, err := opts.ToProject(ctx, nil)
182190
if err != nil {
183191
return err
184192
}
@@ -205,9 +213,9 @@ func runHash(streams api.Streams, opts configOptions) error {
205213
return nil
206214
}
207215

208-
func runProfiles(streams api.Streams, opts configOptions, services []string) error {
216+
func runProfiles(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
209217
set := map[string]struct{}{}
210-
project, err := opts.ToProject(services)
218+
project, err := opts.ToProject(ctx, services)
211219
if err != nil {
212220
return err
213221
}
@@ -227,8 +235,8 @@ func runProfiles(streams api.Streams, opts configOptions, services []string) err
227235
return nil
228236
}
229237

230-
func runConfigImages(streams api.Streams, opts configOptions, services []string) error {
231-
project, err := opts.ToProject(services)
238+
func runConfigImages(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
239+
project, err := opts.ToProject(ctx, services)
232240
if err != nil {
233241
return err
234242
}

pkg/remote/git.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
Copyright 2020 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 remote
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"os/exec"
24+
"path/filepath"
25+
"regexp"
26+
"strconv"
27+
28+
"github.com/compose-spec/compose-go/cli"
29+
"github.com/compose-spec/compose-go/loader"
30+
"github.com/compose-spec/compose-go/types"
31+
"github.com/docker/compose/v2/pkg/api"
32+
"github.com/moby/buildkit/util/gitutil"
33+
"github.com/pkg/errors"
34+
)
35+
36+
func GitRemoteLoaderEnabled() (bool, error) {
37+
if v := os.Getenv("COMPOSE_EXPERIMENTAL_GIT_REMOTE"); v != "" {
38+
enabled, err := strconv.ParseBool(v)
39+
if err != nil {
40+
return false, errors.Wrap(err, "COMPOSE_EXPERIMENTAL_GIT_REMOTE environment variable expects boolean value")
41+
}
42+
return enabled, err
43+
}
44+
return false, nil
45+
}
46+
47+
func NewGitRemoteLoader() (loader.ResourceLoader, error) {
48+
var base string
49+
if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" {
50+
base = cacheHome
51+
} else {
52+
home, err := os.UserHomeDir()
53+
if err != nil {
54+
return nil, err
55+
}
56+
base = filepath.Join(home, ".cache")
57+
}
58+
cache := filepath.Join(base, "docker-compose")
59+
60+
err := os.MkdirAll(cache, 0o700)
61+
return gitRemoteLoader{
62+
cache: cache,
63+
}, err
64+
}
65+
66+
type gitRemoteLoader struct {
67+
cache string
68+
}
69+
70+
func (g gitRemoteLoader) Accept(path string) bool {
71+
_, err := gitutil.ParseGitRef(path)
72+
return err == nil
73+
}
74+
75+
var commitSHA = regexp.MustCompile(`^[a-f0-9]{40}$`)
76+
77+
func (g gitRemoteLoader) Load(ctx context.Context, path string) (string, error) {
78+
ref, err := gitutil.ParseGitRef(path)
79+
if err != nil {
80+
return "", err
81+
}
82+
83+
if ref.Commit == "" {
84+
ref.Commit = "HEAD" // default branch
85+
}
86+
87+
if !commitSHA.MatchString(ref.Commit) {
88+
cmd := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", ref.Remote, ref.Commit)
89+
cmd.Env = g.gitCommandEnv()
90+
out, err := cmd.Output()
91+
if err != nil {
92+
if cmd.ProcessState.ExitCode() == 2 {
93+
return "", errors.Wrapf(err, "repository does not contain ref %s, output: %q", path, string(out))
94+
}
95+
return "", err
96+
}
97+
if len(out) < 40 {
98+
return "", fmt.Errorf("unexpected git command output: %q", string(out))
99+
}
100+
sha := string(out[:40])
101+
if !commitSHA.MatchString(sha) {
102+
return "", fmt.Errorf("invalid commit sha %q", sha)
103+
}
104+
ref.Commit = sha
105+
}
106+
107+
local := filepath.Join(g.cache, ref.Commit)
108+
if _, err := os.Stat(local); os.IsNotExist(err) {
109+
err = g.checkout(ctx, local, ref)
110+
if err != nil {
111+
return "", err
112+
}
113+
}
114+
115+
if ref.SubDir != "" {
116+
local = filepath.Join(local, ref.SubDir)
117+
}
118+
stat, err := os.Stat(local)
119+
if err != nil {
120+
return "", err
121+
}
122+
if stat.IsDir() {
123+
local, err = findFile(cli.DefaultFileNames, local)
124+
}
125+
return local, err
126+
}
127+
128+
func (g gitRemoteLoader) checkout(ctx context.Context, path string, ref *gitutil.GitRef) error {
129+
err := os.MkdirAll(path, 0o700)
130+
if err != nil {
131+
return err
132+
}
133+
err = exec.CommandContext(ctx, "git", "init", path).Run()
134+
if err != nil {
135+
return err
136+
}
137+
138+
cmd := exec.CommandContext(ctx, "git", "remote", "add", "origin", ref.Remote)
139+
cmd.Dir = path
140+
err = cmd.Run()
141+
if err != nil {
142+
return err
143+
}
144+
145+
cmd = exec.CommandContext(ctx, "git", "fetch", "--depth=1", "origin", ref.Commit)
146+
cmd.Env = g.gitCommandEnv()
147+
cmd.Dir = path
148+
err = cmd.Run()
149+
if err != nil {
150+
return err
151+
}
152+
153+
cmd = exec.CommandContext(ctx, "git", "checkout", ref.Commit)
154+
cmd.Dir = path
155+
err = cmd.Run()
156+
if err != nil {
157+
return err
158+
}
159+
return nil
160+
}
161+
162+
func (g gitRemoteLoader) gitCommandEnv() []string {
163+
env := types.NewMapping(os.Environ())
164+
if env["GIT_TERMINAL_PROMPT"] == "" {
165+
// Disable prompting for passwords by Git until user explicitly asks for it.
166+
env["GIT_TERMINAL_PROMPT"] = "0"
167+
}
168+
if env["GIT_SSH"] == "" && env["GIT_SSH_COMMAND"] == "" {
169+
// Disable any ssh connection pooling by Git and do not attempt to prompt the user.
170+
env["GIT_SSH_COMMAND"] = "ssh -o ControlMaster=no -o BatchMode=yes"
171+
}
172+
v := values(env)
173+
return v
174+
}
175+
176+
func findFile(names []string, pwd string) (string, error) {
177+
for _, n := range names {
178+
f := filepath.Join(pwd, n)
179+
if fi, err := os.Stat(f); err == nil && !fi.IsDir() {
180+
return f, nil
181+
}
182+
}
183+
return "", api.ErrNotFound
184+
}
185+
186+
var _ loader.ResourceLoader = gitRemoteLoader{}
187+
188+
func values(m types.Mapping) []string {
189+
values := make([]string, 0, len(m))
190+
for k, v := range m {
191+
values = append(values, fmt.Sprintf("%s=%s", k, v))
192+
}
193+
return values
194+
}

0 commit comments

Comments
 (0)