diff --git a/internal/opensandboxbackend/backend.go b/internal/opensandboxbackend/backend.go index 078ddec..d81d780 100644 --- a/internal/opensandboxbackend/backend.go +++ b/internal/opensandboxbackend/backend.go @@ -3,9 +3,13 @@ package opensandboxbackend import ( "context" + "encoding/base64" + "encoding/json" "errors" "fmt" "net/http" + "sort" + "strings" "time" opensandbox "github.com/alibaba/OpenSandbox/sdks/sandbox/go" @@ -17,6 +21,8 @@ import ( const assignmentMetadata = "ci.ddupan.top/assignment-id" const terminalMetadata = "ci.ddupan.top/terminal" +const annotationsMetadataPrefix = "ci.ddupan.top/annotations-" +const metadataValueLimit = 63 type Lifecycle interface { ListSandboxes(context.Context, opensandbox.ListOptions) (*opensandbox.ListSandboxesResponse, error) @@ -123,7 +129,11 @@ func (b Backend) RecoverAssignments(ctx context.Context, trustDomain string) ([] if sandbox.Metadata[terminalMetadata] == "true" { continue } - assignment, err := taskassignment.FromMetadata(sandbox.Metadata, sandbox.Metadata, trustDomain) + annotations, err := decodeAnnotations(sandbox.Metadata) + if err != nil { + return nil, fmt.Errorf("decode sandbox %s annotations: %w", sandbox.ID, err) + } + assignment, err := taskassignment.FromMetadata(sandbox.Metadata, annotations, trustDomain) if err != nil { return nil, fmt.Errorf("recover sandbox %s: %w", sandbox.ID, err) } @@ -169,8 +179,12 @@ func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignmen for key, value := range launch.Environment { environment[key] = value } - sandboxMetadata := clone(launch.Metadata.Annotations) - for key, value := range launch.Metadata.Labels { + sandboxMetadata := clone(launch.Metadata.Labels) + annotations, err := encodeAnnotations(launch.Metadata.Annotations) + if err != nil { + return nil, fmt.Errorf("encode sandbox annotations: %w", err) + } + for key, value := range annotations { sandboxMetadata[key] = value } request := opensandbox.CreateSandboxRequest{ @@ -194,7 +208,11 @@ func (b Backend) BindIdentity(ctx context.Context, executor *taskworker.Executor if err != nil { return fmt.Errorf("verify sandbox identity metadata: %w", err) } - if sandbox.Metadata["ci.ddupan.top/spiffe-id"] != identity.SPIFFEID { + annotations, err := decodeAnnotations(sandbox.Metadata) + if err != nil { + return fmt.Errorf("decode sandbox identity metadata: %w", err) + } + if annotations["ci.ddupan.top/spiffe-id"] != identity.SPIFFEID { return fmt.Errorf("sandbox %s has inconsistent SPIFFE identity metadata", executor.Name) } return nil @@ -243,3 +261,50 @@ func clone(source map[string]string) map[string]string { } return result } + +// OpenSandbox metadata follows Kubernetes label-value constraints, unlike Pod +// annotations. Store the annotation map as deterministic URL-safe base64 +// chunks so repository paths and SPIFFE IDs remain lossless and recoverable. +func encodeAnnotations(annotations map[string]string) (map[string]string, error) { + data, err := json.Marshal(annotations) + if err != nil { + return nil, err + } + encoded := base64.RawURLEncoding.EncodeToString(data) + result := make(map[string]string, (len(encoded)+metadataValueLimit-1)/metadataValueLimit) + for index := 0; len(encoded) > 0; index++ { + length := min(metadataValueLimit, len(encoded)) + result[fmt.Sprintf("%s%03d", annotationsMetadataPrefix, index)] = encoded[:length] + encoded = encoded[length:] + } + return result, nil +} + +func decodeAnnotations(metadata map[string]string) (map[string]string, error) { + keys := make([]string, 0) + for key := range metadata { + if strings.HasPrefix(key, annotationsMetadataPrefix) { + keys = append(keys, key) + } + } + if len(keys) == 0 { + return nil, errors.New("sandbox annotation metadata is missing") + } + sort.Strings(keys) + var encoded strings.Builder + for index, key := range keys { + if key != fmt.Sprintf("%s%03d", annotationsMetadataPrefix, index) { + return nil, errors.New("sandbox annotation metadata chunks are incomplete") + } + encoded.WriteString(metadata[key]) + } + data, err := base64.RawURLEncoding.DecodeString(encoded.String()) + if err != nil { + return nil, err + } + var annotations map[string]string + if err := json.Unmarshal(data, &annotations); err != nil { + return nil, err + } + return annotations, nil +} diff --git a/internal/opensandboxbackend/backend_test.go b/internal/opensandboxbackend/backend_test.go index 703265c..34f188c 100644 --- a/internal/opensandboxbackend/backend_test.go +++ b/internal/opensandboxbackend/backend_test.go @@ -31,7 +31,8 @@ func (f *fakeLifecycle) GetSandbox(_ context.Context, id string) (*opensandbox.S return &item, nil } } - return &opensandbox.SandboxInfo{ID: id, Metadata: map[string]string{"ci.ddupan.top/spiffe-id": assignment().Identity.SPIFFEID}}, nil + metadata, _ := encodeAnnotations(map[string]string{"ci.ddupan.top/spiffe-id": assignment().Identity.SPIFFEID}) + return &opensandbox.SandboxInfo{ID: id, Metadata: metadata}, nil } func (f *fakeLifecycle) PatchSandboxMetadata(_ context.Context, id string, patch opensandbox.MetadataPatch) (*opensandbox.SandboxInfo, error) { for index := range f.items { @@ -95,6 +96,32 @@ func TestCreateUsesPoolAndPersistsRecoveryMetadata(t *testing.T) { if lifecycle.created.Env["CI_RUNNER_CAPABILITY"] != "capability" { t.Fatalf("environment = %#v", lifecycle.created.Env) } + annotations, err := decodeAnnotations(lifecycle.created.Metadata) + if err != nil || annotations["ci.ddupan.top/repository"] != "owner/repo" || annotations["ci.ddupan.top/spiffe-id"] != assignment().Identity.SPIFFEID { + t.Fatalf("annotations=%#v err=%v", annotations, err) + } + for _, value := range lifecycle.created.Metadata { + if len(value) > metadataValueLimit { + t.Fatalf("metadata value exceeds %d characters: %q", metadataValueLimit, value) + } + } +} + +func TestAnnotationMetadataRoundTripPreservesSlashValues(t *testing.T) { + want := taskworker.BackendMetadata(assignment()).Annotations + encoded, err := encodeAnnotations(want) + if err != nil { + t.Fatal(err) + } + got, err := decodeAnnotations(encoded) + if err != nil { + t.Fatal(err) + } + for key, value := range want { + if got[key] != value { + t.Fatalf("%s=%q, want %q", key, got[key], value) + } + } } func TestBindIdentityVerifiesPersistedMetadata(t *testing.T) {