|
1 | 1 | package main
|
2 | 2 |
|
3 | 3 | import (
|
4 |
| - "bufio" |
5 | 4 | "context"
|
6 |
| - "errors" |
7 |
| - "flag" |
8 | 5 | "fmt"
|
9 |
| - "io" |
10 |
| - "net/http" |
11 |
| - "net/url" |
12 | 6 | "os"
|
13 |
| - "os/exec" |
14 | 7 | "os/signal"
|
15 |
| - "path/filepath" |
16 |
| - "slices" |
17 |
| - "strings" |
18 | 8 | "syscall"
|
19 |
| - "time" |
20 | 9 |
|
21 |
| - "github.com/cenkalti/backoff" |
22 |
| - "github.com/go-git/go-git/v5" |
23 |
| - "github.com/google/go-github/v62/github" |
24 |
| - "github.com/google/uuid" |
25 |
| - "github.com/hashicorp/go-slug" |
26 |
| - "github.com/nimbolus/terraform-backend/pkg/tfcontext" |
27 |
| - giturls "github.com/whilp/git-urls" |
| 10 | + "github.com/nimbolus/terraform-backend/pkg/fs" |
| 11 | + "github.com/nimbolus/terraform-backend/pkg/scaffold" |
| 12 | + "github.com/nimbolus/terraform-backend/pkg/speculative" |
28 | 13 | )
|
29 | 14 |
|
30 |
| -func serveWorkspace(ctx context.Context) (string, error) { |
| 15 | +func main() { |
31 | 16 | cwd, err := os.Getwd()
|
32 | 17 | if err != nil {
|
33 |
| - return "", err |
34 |
| - } |
35 |
| - |
36 |
| - backend, err := tfcontext.FindBackend(cwd) |
37 |
| - if err != nil { |
38 |
| - return "", err |
39 |
| - } |
40 |
| - backendURL, err := url.Parse(backend.Address) |
41 |
| - if err != nil { |
42 |
| - return "", fmt.Errorf("failed to parse backend url: %s, %w", backend.Address, err) |
43 |
| - } |
44 |
| - if backend.Password == "" { |
45 |
| - backendPassword, ok := os.LookupEnv("TF_HTTP_PASSWORD") |
46 |
| - if !ok || backendPassword == "" { |
47 |
| - return "", errors.New("missing backend password") |
48 |
| - } |
49 |
| - backend.Password = backendPassword |
50 |
| - } |
51 |
| - |
52 |
| - id := uuid.New() |
53 |
| - backendURL.Path = filepath.Join(backendURL.Path, "/share/", id.String()) |
54 |
| - |
55 |
| - pr, pw := io.Pipe() |
56 |
| - req, err := http.NewRequestWithContext(ctx, http.MethodPost, backendURL.String(), pr) |
57 |
| - if err != nil { |
58 |
| - return "", err |
| 18 | + panic(fmt.Errorf("failed to get working directory: %w", err)) |
59 | 19 | }
|
60 |
| - req.Header.Set("Content-Type", "application/octet-stream") |
61 |
| - req.SetBasicAuth(backend.Username, backend.Password) |
62 |
| - |
63 |
| - go func() { |
64 |
| - _, err := slug.Pack(cwd, pw, true) |
65 |
| - if err != nil { |
66 |
| - fmt.Printf("failed to pack workspace: %v\n", err) |
67 |
| - pw.CloseWithError(err) |
68 |
| - } else { |
69 |
| - pw.Close() |
70 |
| - } |
71 |
| - }() |
72 |
| - |
73 |
| - go func() { |
74 |
| - resp, err := http.DefaultClient.Do(req) |
75 |
| - if err != nil { |
76 |
| - fmt.Printf("failed to stream workspace: %v\n", err) |
77 |
| - } else if resp.StatusCode/100 != 2 { |
78 |
| - fmt.Printf("invalid status code after streaming workspace: %d\n", resp.StatusCode) |
79 |
| - } |
80 |
| - fmt.Println("done streaming workspace") |
81 |
| - }() |
82 |
| - |
83 |
| - return backendURL.String(), nil |
84 |
| -} |
85 | 20 |
|
86 |
| -type countingReader struct { |
87 |
| - io.Reader |
88 |
| - readBytes int |
89 |
| -} |
90 |
| - |
91 |
| -func (c *countingReader) Read(dst []byte) (int, error) { |
92 |
| - n, err := c.Reader.Read(dst) |
93 |
| - c.readBytes += n |
94 |
| - return n, err |
95 |
| -} |
96 |
| - |
97 |
| -var ignoredGroupNames = []string{ |
98 |
| - "Operating System", |
99 |
| - "Runner Image", |
100 |
| - "Runner Image Provisioner", |
101 |
| - "GITHUB_TOKEN Permissions", |
102 |
| -} |
| 21 | + rootCmd := speculative.NewCommand() |
| 22 | + rootCmd.AddCommand(scaffold.NewCommand(fs.ForOS(cwd), os.Stdin)) |
103 | 23 |
|
104 |
| -func streamLogs(logsURL *url.URL, skip int64) (int64, error) { |
105 |
| - logs, err := http.Get(logsURL.String()) |
106 |
| - if err != nil { |
107 |
| - return 0, err |
108 |
| - } |
109 |
| - if logs.StatusCode != http.StatusOK { |
110 |
| - return 0, fmt.Errorf("invalid status for logs: %d", logs.StatusCode) |
111 |
| - } |
112 |
| - defer logs.Body.Close() |
113 |
| - |
114 |
| - if _, err := io.Copy(io.Discard, io.LimitReader(logs.Body, skip)); err != nil { |
115 |
| - return 0, err |
116 |
| - } |
117 |
| - |
118 |
| - r := &countingReader{Reader: logs.Body} |
119 |
| - scanner := bufio.NewScanner(r) |
120 |
| - groupDepth := 0 |
121 |
| - for scanner.Scan() { |
122 |
| - line := scanner.Text() |
123 |
| - ts, rest, ok := strings.Cut(line, " ") |
124 |
| - if !ok { |
125 |
| - rest = ts |
126 |
| - } |
127 |
| - if groupName, ok := strings.CutPrefix(rest, "##[group]"); ok { |
128 |
| - groupDepth++ |
129 |
| - if !slices.Contains(ignoredGroupNames, groupName) { |
130 |
| - fmt.Printf("\n# %s\n", groupName) |
131 |
| - } |
132 |
| - } |
133 |
| - if groupDepth == 0 { |
134 |
| - fmt.Println(rest) |
135 |
| - } |
136 |
| - if strings.HasPrefix(rest, "##[endgroup]") { |
137 |
| - groupDepth-- |
138 |
| - } |
139 |
| - } |
140 |
| - if err := scanner.Err(); err != nil { |
141 |
| - return int64(r.readBytes), err |
142 |
| - } |
143 |
| - |
144 |
| - return int64(r.readBytes), err |
145 |
| -} |
146 |
| - |
147 |
| -var ( |
148 |
| - owner string |
149 |
| - repo string |
150 |
| - workflowFilename string |
151 |
| -) |
152 |
| - |
153 |
| -func gitRepoOrigin() (*url.URL, error) { |
154 |
| - cwd, err := os.Getwd() |
155 |
| - if err != nil { |
156 |
| - return nil, err |
157 |
| - } |
158 |
| - |
159 |
| - repo, err := git.PlainOpen(cwd) |
160 |
| - if err != nil { |
161 |
| - return nil, err |
162 |
| - } |
163 |
| - |
164 |
| - orig, err := repo.Remote("origin") |
165 |
| - if err != nil { |
166 |
| - return nil, err |
167 |
| - } |
168 |
| - if orig == nil { |
169 |
| - return nil, errors.New("origin remote not present") |
170 |
| - } |
171 |
| - |
172 |
| - for _, u := range orig.Config().URLs { |
173 |
| - remoteURL, err := giturls.Parse(u) |
174 |
| - if err != nil { |
175 |
| - continue |
176 |
| - } |
177 |
| - if remoteURL.Hostname() == "github.com" { |
178 |
| - return remoteURL, nil |
179 |
| - } |
180 |
| - } |
181 |
| - return nil, errors.New("no suitable url found") |
182 |
| -} |
183 |
| - |
184 |
| -func main() { |
185 | 24 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
186 | 25 | defer cancel()
|
187 | 26 |
|
188 |
| - flag.StringVar(&owner, "github-owner", "", "Repository owner") |
189 |
| - flag.StringVar(&repo, "github-repo", "", "Repository name") |
190 |
| - flag.StringVar(&workflowFilename, "workflow-file", "preview.yaml", "Name of the workflow file to run for previews") |
191 |
| - flag.Parse() |
192 |
| - |
193 |
| - if owner == "" || repo == "" { |
194 |
| - if ghURL, err := gitRepoOrigin(); err == nil { |
195 |
| - parts := strings.Split(ghURL.Path, "/") |
196 |
| - if len(parts) >= 2 { |
197 |
| - owner = parts[0] |
198 |
| - repo = strings.TrimSuffix(parts[1], ".git") |
199 |
| - fmt.Printf("Using local repo info: %s/%s\n", owner, repo) |
200 |
| - } |
201 |
| - } |
202 |
| - } |
203 |
| - if owner == "" { |
204 |
| - panic("Missing flag: -github-owner") |
205 |
| - } |
206 |
| - if repo == "" { |
207 |
| - panic("Missing flag: -github-repo") |
208 |
| - } |
209 |
| - |
210 |
| - serverURL, err := serveWorkspace(ctx) |
211 |
| - if err != nil { |
212 |
| - panic(err) |
213 |
| - } |
214 |
| - |
215 |
| - // steal token from GH CLI |
216 |
| - cmd := exec.CommandContext(ctx, "gh", "auth", "token") |
217 |
| - out, err := cmd.Output() |
218 |
| - if err != nil { |
219 |
| - panic(err) |
220 |
| - } |
221 |
| - |
222 |
| - token := strings.TrimSpace(string(out)) |
223 |
| - gh := github.NewClient(nil).WithAuthToken(token) |
224 |
| - |
225 |
| - startedAt := time.Now().UTC() |
226 |
| - |
227 |
| - // start workflow |
228 |
| - _, err = gh.Actions.CreateWorkflowDispatchEventByFileName(ctx, |
229 |
| - owner, repo, workflowFilename, |
230 |
| - github.CreateWorkflowDispatchEventRequest{ |
231 |
| - Ref: "main", |
232 |
| - Inputs: map[string]interface{}{ |
233 |
| - "workspace_transfer_url": serverURL, |
234 |
| - }, |
235 |
| - }, |
236 |
| - ) |
237 |
| - if err != nil { |
238 |
| - panic(err) |
239 |
| - } |
240 |
| - |
241 |
| - fmt.Println("Waiting for run to start...") |
242 |
| - |
243 |
| - // find workflow run |
244 |
| - var run *github.WorkflowRun |
245 |
| - err = backoff.Retry(func() error { |
246 |
| - workflows, _, err := gh.Actions.ListWorkflowRunsByFileName( |
247 |
| - ctx, owner, repo, workflowFilename, |
248 |
| - &github.ListWorkflowRunsOptions{ |
249 |
| - Created: fmt.Sprintf(">=%s", startedAt.Format("2006-01-02T15:04")), |
250 |
| - }, |
251 |
| - ) |
252 |
| - if err != nil { |
253 |
| - return backoff.Permanent(err) |
254 |
| - } |
255 |
| - if len(workflows.WorkflowRuns) == 0 { |
256 |
| - return fmt.Errorf("no workflow runs found") |
257 |
| - } |
258 |
| - |
259 |
| - run = workflows.WorkflowRuns[0] |
260 |
| - return nil |
261 |
| - }, backoff.NewExponentialBackOff()) |
262 |
| - if err != nil { |
263 |
| - panic(err) |
264 |
| - } |
265 |
| - |
266 |
| - var jobID int64 |
267 |
| - err = backoff.Retry(func() error { |
268 |
| - jobs, _, err := gh.Actions.ListWorkflowJobs(ctx, |
269 |
| - owner, repo, *run.ID, |
270 |
| - &github.ListWorkflowJobsOptions{}, |
271 |
| - ) |
272 |
| - if err != nil { |
273 |
| - return backoff.Permanent(err) |
274 |
| - } |
275 |
| - if len(jobs.Jobs) == 0 { |
276 |
| - return fmt.Errorf("no jobs found") |
277 |
| - } |
278 |
| - |
279 |
| - jobID = *jobs.Jobs[0].ID |
280 |
| - return nil |
281 |
| - }, backoff.NewExponentialBackOff()) |
282 |
| - if err != nil { |
283 |
| - panic(err) |
284 |
| - } |
285 |
| - |
286 |
| - logsURL, _, err := gh.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 2) |
287 |
| - if err != nil { |
288 |
| - panic(err) |
289 |
| - } |
290 |
| - |
291 |
| - var readBytes int64 |
292 |
| - for { |
293 |
| - n, err := streamLogs(logsURL, readBytes) |
294 |
| - if err != nil { |
295 |
| - panic(err) |
296 |
| - } |
297 |
| - readBytes += n |
298 |
| - |
299 |
| - // check if job is done |
300 |
| - job, _, err := gh.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID) |
301 |
| - if err != nil { |
302 |
| - panic(err) |
303 |
| - } |
304 |
| - if job.CompletedAt != nil { |
305 |
| - fmt.Println("Job complete.") |
306 |
| - break |
307 |
| - } |
| 27 | + if err := rootCmd.ExecuteContext(ctx); err != nil { |
| 28 | + os.Exit(1) |
308 | 29 | }
|
309 | 30 | }
|
0 commit comments