Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Let a job declare the secrets it reads

4ac42680eca3181efc70099f65d6e2c9f45e9dd2 · Keenan Nemetz · 7d ago · parent 0c40a1d3

Let a job declare the secrets it reads

A job now names the repository secrets it needs, and the instance delivers
those values when the job is leased. The workflow carries names only, so the
queue row, the metadata branch and the run record never hold a value: the
record names the version of each value the job ran with, which is what makes a
result say which one it used.

    jobs:
      deploy:
        image: alpine:3.22
        secrets: [DEPLOY_KEY]

A name the repository has no value for is not a failure. The variable is left
unset rather than empty, the step's shell can tell the difference, and the
runner writes one line to the log saying which value was missing. That keeps
an optional secret optional and leaves "required" with the step, where someone
can write the message: ${DEPLOY_KEY:?DEPLOY_KEY is not set}.

The value reaches the step through a file the runner writes into the run
directory it already mounts at /run/vole for an event payload, and each step is
prefixed with `. /run/vole/secrets.env`. That is deliberately not an env-file
argument to the runtime, which cannot carry a line break, and not --env, which
puts the value in the process's argument list where every process on the host
can read it. A local `vole workflow run` writes the same file, reading the
values with the developer's own credential, and says which ones it could not
read.

The name rule and the 128 KiB limit moved to ci, next to the environment rules
they mirror, so the store refuses a name no workflow could declare and the
workflow refuses a name no process environment could carry.

Slice 2 of #154.

server/ssh/secret.go +43

@@ -25,6 +25,7 @@ import (
25 25
26 26 xssh "golang.org/x/crypto/ssh"
27 27
28 "github.com/rodent-software/vole/ci/runs"
28 29 "github.com/rodent-software/vole/forge/secret"
29 30 )
30 31
@@ -152,3 +153,45 @@ func secretProblem(err error, secretName, repoName string) string {
152 153 }
153 154 return err.Error()
154 155 }
156
157 // leaseSecrets resolves what a leased job declared. The queue row holds names, so
158 // this is the one place a value leaves the store on its way to a job, and the store
159 // records the read against the account holding the lease.
160 //
161 // A name the repository has no value for is not a failure. The job runs without
162 // that variable and the runner says which one it went without, because a workflow
163 // is allowed to have an optional secret and a required one is the step's
164 // ${NAME:?}. A value that cannot be read for another reason — a database restored
165 // beside a new key — is reported the same way, with the reason, so the job is not
166 // stopped by an instance problem it cannot fix.
167 func (s *Server) leaseSecrets(run *runs.Run, actor string) (map[string]runnerSecret, []runnerSecretIssue) {
168 if len(run.Secrets) == 0 {
169 return nil, nil
170 }
171 var store *secret.Store
172 if s.cfg.Instance != nil {
173 store = s.cfg.Instance.Secrets
174 }
175 meta, metaErr := s.cfg.Repos.Meta(run.Repo)
176 delivered := make(map[string]runnerSecret, len(run.Secrets))
177 var missing []runnerSecretIssue
178 for _, name := range run.Secrets {
179 switch {
180 case metaErr != nil:
181 missing = append(missing, runnerSecretIssue{Name: name, Reason: metaErr.Error()})
182 case store == nil:
183 missing = append(missing, runnerSecretIssue{Name: name, Reason: "this instance has no secret store"})
184 default:
185 value, version, err := store.Read(meta.ID, name, actor, "run="+run.ID)
186 switch {
187 case errors.Is(err, secret.ErrNotFound):
188 missing = append(missing, runnerSecretIssue{Name: name})
189 case err != nil:
190 missing = append(missing, runnerSecretIssue{Name: name, Reason: err.Error()})
191 default:
192 delivered[name] = runnerSecret{Version: version, Value: value}
193 }
194 }
195 }
196 return delivered, missing
197 }