···99 securejoin "github.com/cyphar/filepath-securejoin"
1010 enry "github.com/go-enry/go-enry/v2"
1111 "tangled.org/core/api/tangled"
1212+ "tangled.org/core/hostutil"
1213)
13141415type Repo struct {
···196197197198func StripGitExt(name string) string {
198199 return strings.TrimSuffix(name, ".git")
200200+}
201201+202202+// ValidateSpindle normalizes a user-typed spindle host. Empty means "no spindle".
203203+//
204204+// Membership is enforced by the spindle itself, so this only checks that the value
205205+// is a host the appview can safely send service-auth requests to.
206206+func ValidateSpindle(raw string, dev bool) (string, error) {
207207+ raw = strings.TrimSpace(raw)
208208+ if raw == "" {
209209+ return "", nil
210210+ }
211211+212212+ host, noTLS, err := hostutil.ParseHostname(raw)
213213+ if err != nil {
214214+ return "", fmt.Errorf("%q is not a valid spindle host", raw)
215215+ }
216216+217217+ // ParseHostname allows localhost:PORT, which would make the appview dial itself
218218+ if noTLS && !dev {
219219+ return "", fmt.Errorf("spindle must be a public https host")
220220+ }
221221+222222+ return host, nil
199223}
200224201225type RepoGroup struct {
···2020 <div class="col-span-1 md:col-span-2">
2121 <h2 class="text-lg pb-2 font-medium">Spindle</h2>
2222 <p class="text-gray-500 dark:text-gray-400">
2323- Choose a spindle to execute your workflows on. Only repository owners
2424- can configure spindles. Spindles can be selfhosted,
2323+ The spindle to execute your workflows on; leave it empty to disable
2424+ pipelines. Its operator has to have allowed you as a member, otherwise
2525+ it will ignore this repository. Only repository owners can configure
2626+ spindles. Spindles can be selfhosted,
2527 <a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide">
2628 click to learn more.
2729 </a>
···3335 </div>
3436 {{ else }}
3537 <form hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch">
3636- <select
3737- id="spindle"
3838- name="spindle"
3939- required
4040- class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
4141- {{/* For some reason, we can't use an empty string in a <select> in all scenarios unless it is preceded by a disabled select?? No idea, could just be a Firefox thing? */}}
4242- <option value="[[none]]" class="py-1" {{ if not $.CurrentSpindle }}selected{{ end }}>
4343- {{ if not $.CurrentSpindle }}
4444- Choose a spindle
4545- {{ else }}
4646- Disable pipelines
4747- {{ end }}
4848- </option>
4949- {{ range $.Spindles }}
5050- <option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>
5151- {{ . }}
5252- </option>
5353- {{ end }}
5454- </select>
3838+ {{ template "repo/fragments/spindleInput"
3939+ (dict "Spindles" $.Spindles "Current" $.CurrentSpindle "Class" "p-1 max-w-64") }}
5540 <button class="btn flex gap-2 items-center" type="submit" {{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>
5641 {{ i "check" "size-4" }}
5742 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
···114114 return
115115 }
116116117117- newSpindle := r.FormValue("spindle")
118118- removingSpindle := newSpindle == "[[none]]" // see pages/templates/repo/settings/pipelines.html for more info on why we use this value
117117+ // an empty field removes the spindle; membership is the spindle's call, we
118118+ // only check that the value is a host we can talk to
119119+ newSpindle, err := models.ValidateSpindle(r.FormValue("spindle"), rp.config.Core.Dev)
120120+ if err != nil {
121121+ rp.pages.Notice(w, errorId, err.Error())
122122+ return
123123+ }
124124+ removingSpindle := newSpindle == ""
125125+119126 client, err := rp.oauth.AuthorizedClient(r)
120127 if err != nil {
121128 fail("Failed to authorize. Try again later.", err)
122129 return
123123- }
124124-125125- if !removingSpindle {
126126- // ensure that this is a valid spindle for this user
127127- validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
128128- if err != nil {
129129- fail("Failed to find spindles. Try again later.", err)
130130- return
131131- }
132132-133133- if !slices.Contains(validSpindles, newSpindle) {
134134- fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))
135135- return
136136- }
137130 }
138131139132 newRepo := *f
···14451438 return
14461439 }
1447144014481448- // optional spindle selection; validate the user is a member if provided
14491449- spindle := r.FormValue("spindle")
14501450- if spindle != "" {
14511451- validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
14521452- if err != nil {
14531453- l.Error("failed to fetch spindles", "err", err)
14541454- rp.pages.Notice(w, "repo", "Failed to configure spindle. Try again later.")
14551455- return
14561456- }
14571457- if !slices.Contains(validSpindles, spindle) {
14581458- rp.pages.Notice(w, "repo", "Invalid spindle selection.")
14591459- return
14601460- }
14411441+ // optional spindle selection; the spindle itself decides whether to accept
14421442+ // this repo, we only check that the value is a host we can talk to
14431443+ spindle, err := models.ValidateSpindle(r.FormValue("spindle"), rp.config.Core.Dev)
14441444+ if err != nil {
14451445+ rp.pages.Notice(w, "repo", err.Error())
14461446+ return
14611447 }
1462144814631449 // choose a name for a fork
···1542154215431543Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
1544154415451545+Spindles are not registered with the appview. To point a repository at
15461546+yours, type its hostname into the spindle field under the repository's
15471547+pipeline settings (or when creating or forking a repo); recently used
15481548+spindles are offered as suggestions. The spindle picks the repo up from
15491549+the network and runs its pipelines if its owner is a member.
15501550+15451551### Managing members
1546155215471553An invite-only spindle (the default, see `SPINDLE_SERVER_INVITE_ONLY`) only
···26922698```
2693269926942700The above VM should already be running a spindle on
26952695-`localhost:6555`. Head to http://localhost:3000/settings/spindles and
26962696-hit "Verify". You can then configure each repository to use
26972697-this spindle and run CI jobs.
27012701+`localhost:6555`. Spindles aren't registered with the appview:
27022702+type `localhost:6555` into the spindle field on a repository's
27032703+pipeline settings (or when creating the repo) and it will run
27042704+that repo's CI jobs, as long as the spindle allows you as a
27052705+member (see [Managing members](#managing-members), or run it
27062706+with `SPINDLE_SERVER_INVITE_ONLY=false`).
2698270726992708Of interest when debugging spindles:
27002709