建立 Go Task Scheduler 协议骨架

This commit is contained in:
2026-09-20 17:52:38 +00:00
parent 5e5f2cc48d
commit 3c1fca1832
10 changed files with 394 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
// Package taskidentity derives workload identities from tasks assigned by Gitea.
package taskidentity
import (
"crypto/sha256"
"errors"
"fmt"
"regexp"
"strings"
runnerv1 "gitea.dev/actionslib/runner/v1"
)
var safeSegment = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
// Identity is the trusted identity context extracted from a fetched task.
type Identity struct {
Repository string
Task string
SPIFFEID string
}
// FromTask derives the repository/task SPIFFE ID from Gitea's trusted task
// context. Workflow input never supplies or overrides the resulting ID.
func FromTask(task *runnerv1.Task, trustDomain string) (Identity, error) {
if task == nil || task.Context == nil {
return Identity{}, errors.New("task context is required")
}
repository := strings.TrimSpace(task.Context.GetFields()["repository"].GetStringValue())
taskName := strings.TrimSpace(task.Context.GetFields()["job"].GetStringValue())
parts := strings.Split(repository, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return Identity{}, fmt.Errorf("invalid task repository %q", repository)
}
if taskName == "" {
return Identity{}, errors.New("task job is required")
}
trustDomain = strings.TrimSpace(trustDomain)
if trustDomain == "" || strings.ContainsAny(trustDomain, "/ ") {
return Identity{}, fmt.Errorf("invalid trust domain %q", trustDomain)
}
path := strings.Join([]string{
sanitize(parts[0]),
sanitize(parts[1]),
sanitize(taskName),
}, "/")
return Identity{
Repository: repository,
Task: taskName,
SPIFFEID: "spiffe://" + trustDomain + "/ci/" + path,
}, nil
}
// sanitize intentionally matches the bootstrap Python implementation so
// existing Zot and OpenBao policies keep their current identity names.
func sanitize(value string) string {
if safeSegment.MatchString(value) {
return value
}
slug := strings.Trim(regexp.MustCompile(`[^A-Za-z0-9._-]+`).ReplaceAllString(value, "-"), "-._")
if len(slug) > 48 {
slug = strings.TrimRight(slug[:48], "-._")
}
if slug == "" {
slug = "segment"
}
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(value)))[:12]
return slug + "-" + digest
}
+64
View File
@@ -0,0 +1,64 @@
package taskidentity
import (
"testing"
runnerv1 "gitea.dev/actionslib/runner/v1"
"google.golang.org/protobuf/types/known/structpb"
)
func TestFromTaskUsesFetchedContext(t *testing.T) {
ctx, err := structpb.NewStruct(map[string]any{
"repository": "panxiao81/gitea-dynamic-runner",
"job": "publish-images",
})
if err != nil {
t.Fatal(err)
}
got, err := FromTask(&runnerv1.Task{Id: 900, Context: ctx}, "ddupan.top")
if err != nil {
t.Fatal(err)
}
if got.Repository != "panxiao81/gitea-dynamic-runner" || got.Task != "publish-images" {
t.Fatalf("unexpected task identity context: %#v", got)
}
want := "spiffe://ddupan.top/ci/panxiao81/gitea-dynamic-runner/publish-images"
if got.SPIFFEID != want {
t.Fatalf("SPIFFE ID = %q, want %q", got.SPIFFEID, want)
}
}
func TestFromTaskMatchesBootstrapNormalization(t *testing.T) {
ctx, err := structpb.NewStruct(map[string]any{
"repository": "panxiao81/postgresql-tenant-operator",
"job": "Run on Ubuntu",
})
if err != nil {
t.Fatal(err)
}
got, err := FromTask(&runnerv1.Task{Context: ctx}, "ddupan.top")
if err != nil {
t.Fatal(err)
}
want := "spiffe://ddupan.top/ci/panxiao81/postgresql-tenant-operator/Run-on-Ubuntu-8b7cd4c244fb"
if got.SPIFFEID != want {
t.Fatalf("SPIFFE ID = %q, want %q", got.SPIFFEID, want)
}
}
func TestFromTaskRejectsIncompleteServerContext(t *testing.T) {
for _, fields := range []map[string]any{
{"repository": "invalid", "job": "test"},
{"repository": "owner/repo", "job": ""},
} {
ctx, err := structpb.NewStruct(fields)
if err != nil {
t.Fatal(err)
}
if _, err := FromTask(&runnerv1.Task{Context: ctx}, "ddupan.top"); err == nil {
t.Fatalf("expected invalid task context to fail: %#v", fields)
}
}
}