|
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