fix: 编码 OpenSandbox assignment metadata
This commit is contained in:
@@ -3,9 +3,13 @@ package opensandboxbackend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
opensandbox "github.com/alibaba/OpenSandbox/sdks/sandbox/go"
|
opensandbox "github.com/alibaba/OpenSandbox/sdks/sandbox/go"
|
||||||
@@ -17,6 +21,8 @@ import (
|
|||||||
|
|
||||||
const assignmentMetadata = "ci.ddupan.top/assignment-id"
|
const assignmentMetadata = "ci.ddupan.top/assignment-id"
|
||||||
const terminalMetadata = "ci.ddupan.top/terminal"
|
const terminalMetadata = "ci.ddupan.top/terminal"
|
||||||
|
const annotationsMetadataPrefix = "ci.ddupan.top/annotations-"
|
||||||
|
const metadataValueLimit = 63
|
||||||
|
|
||||||
type Lifecycle interface {
|
type Lifecycle interface {
|
||||||
ListSandboxes(context.Context, opensandbox.ListOptions) (*opensandbox.ListSandboxesResponse, error)
|
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" {
|
if sandbox.Metadata[terminalMetadata] == "true" {
|
||||||
continue
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("recover sandbox %s: %w", sandbox.ID, err)
|
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 {
|
for key, value := range launch.Environment {
|
||||||
environment[key] = value
|
environment[key] = value
|
||||||
}
|
}
|
||||||
sandboxMetadata := clone(launch.Metadata.Annotations)
|
sandboxMetadata := clone(launch.Metadata.Labels)
|
||||||
for key, value := range 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
|
sandboxMetadata[key] = value
|
||||||
}
|
}
|
||||||
request := opensandbox.CreateSandboxRequest{
|
request := opensandbox.CreateSandboxRequest{
|
||||||
@@ -194,7 +208,11 @@ func (b Backend) BindIdentity(ctx context.Context, executor *taskworker.Executor
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("verify sandbox identity metadata: %w", err)
|
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 fmt.Errorf("sandbox %s has inconsistent SPIFFE identity metadata", executor.Name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -243,3 +261,50 @@ func clone(source map[string]string) map[string]string {
|
|||||||
}
|
}
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ func (f *fakeLifecycle) GetSandbox(_ context.Context, id string) (*opensandbox.S
|
|||||||
return &item, nil
|
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) {
|
func (f *fakeLifecycle) PatchSandboxMetadata(_ context.Context, id string, patch opensandbox.MetadataPatch) (*opensandbox.SandboxInfo, error) {
|
||||||
for index := range f.items {
|
for index := range f.items {
|
||||||
@@ -95,6 +96,32 @@ func TestCreateUsesPoolAndPersistsRecoveryMetadata(t *testing.T) {
|
|||||||
if lifecycle.created.Env["CI_RUNNER_CAPABILITY"] != "capability" {
|
if lifecycle.created.Env["CI_RUNNER_CAPABILITY"] != "capability" {
|
||||||
t.Fatalf("environment = %#v", lifecycle.created.Env)
|
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) {
|
func TestBindIdentityVerifiesPersistedMetadata(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user