Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CubeMaster/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/build/
/test/
cmd/cubemastercli/internal/envd/assets/envd
35 changes: 32 additions & 3 deletions CubeMaster/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ BUILD_FLAGS=-ldflags "-s -w \
-X '$(VERSION_PKG).Version=$(CUBE_VERSION)' \
-X '$(VERSION_PKG).Commit=$(CUBE_COMMIT)' \
-X '$(VERSION_PKG).BuildTime=$(CUBE_BUILD_TIME)'"
ENVD_LOCAL_PATH ?=
ENVD_EMBED_ASSET := $(ROOT_DIR)/cmd/cubemastercli/internal/envd/assets/envd
ENVD_BUILD_TAGS := $(if $(strip $(ENVD_LOCAL_PATH)),embed_envd,)
DATE:=$(shell date +'%Y%m%d-%H%M%S')

NAMESPACE?=cubemaster
Expand Down Expand Up @@ -72,13 +75,39 @@ check-proto-tools:
@command -v protoc-gen-go-grpc >/dev/null 2>&1 || { echo "error: protoc-gen-go-grpc not installed"; exit 1; }
@if [ -z "$(PROTOBUF_INCLUDE_DIR)" ]; then echo "error: protobuf include directory not found"; exit 1; fi

.PHONY: prepare-cubemastercli-envd cleanup-cubemastercli-envd
prepare-cubemastercli-envd:
ifneq ($(strip $(ENVD_LOCAL_PATH)),)
@test -f "$(ENVD_LOCAL_PATH)" || { echo "error: ENVD_LOCAL_PATH must point to a regular file: $(ENVD_LOCAL_PATH)"; exit 1; }
@test -s "$(ENVD_LOCAL_PATH)" || { echo "error: ENVD_LOCAL_PATH must not be empty: $(ENVD_LOCAL_PATH)"; exit 1; }
@size=$$(wc -c < "$(ENVD_LOCAL_PATH)"); \
if [ "$$size" -gt 16777216 ]; then \
echo "error: ENVD_LOCAL_PATH exceeds 16MiB: $(ENVD_LOCAL_PATH)"; exit 1; \
fi
@magic=$$(LC_ALL=C dd if="$(ENVD_LOCAL_PATH)" bs=4 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n'); \
if [ "$$magic" != "7f454c46" ]; then \
echo "error: ENVD_LOCAL_PATH must be an ELF binary: $(ENVD_LOCAL_PATH)"; exit 1; \
fi
@mkdir -p "$(dir $(ENVD_EMBED_ASSET))"
@cp "$(ENVD_LOCAL_PATH)" "$(ENVD_EMBED_ASSET)"
endif

cleanup-cubemastercli-envd:
ifneq ($(strip $(ENVD_LOCAL_PATH)),)
@rm -f "$(ENVD_EMBED_ASSET)"
endif

$(APPS): deps
@echo "${GOOS}"
@echo "${CUBE_VERSION}"
$(call msg,Build app,$@)
$(Q)go build -o $(BUILD_DIR)/$@ \
$(BUILD_FLAGS) \
cmd/$@/*.go
$(Q)if [ "$@" = "cubemastercli" ] && [ -n "$(strip $(ENVD_LOCAL_PATH))" ]; then \
$(MAKE) prepare-cubemastercli-envd; \
trap '$(MAKE) cleanup-cubemastercli-envd' EXIT; \
go build -tags "$(ENVD_BUILD_TAGS)" -o $(BUILD_DIR)/$@ $(BUILD_FLAGS) cmd/$@/*.go; \
else \
go build -o $(BUILD_DIR)/$@ $(BUILD_FLAGS) cmd/$@/*.go; \
fi



Expand Down
111 changes: 111 additions & 0 deletions CubeMaster/cmd/cubemastercli/commands/cubebox/envd_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) 2026 Tencent Inc.
// SPDX-License-Identifier: Apache-2.0
//

package cubebox

import (
"bytes"
"errors"
"fmt"
"mime/multipart"
"os"
"strings"

jsoniter "github.com/json-iterator/go"
embeddedenvd "github.com/tencentcloud/CubeSandbox/CubeMaster/cmd/cubemastercli/internal/envd"
"github.com/urfave/cli"
)

const maxEnvdUploadBytes = 16 * 1024 * 1024

type envdUploadPayload struct {
Data []byte
Source string
}

func selectEnvdUploadPayload(c *cli.Context) (*envdUploadPayload, error) {
envdPath := strings.TrimSpace(c.String("envd-path"))
if envdPath != "" && !c.Bool("enable-inject-envd") {
return nil, errors.New("--envd-path requires --enable-inject-envd")
}
if !c.Bool("enable-inject-envd") {
return nil, nil
}
if envdPath != "" {
data, err := readLocalEnvdBinary(envdPath)
if err != nil {
return nil, err
}
return &envdUploadPayload{Data: data, Source: envdPath}, nil
}
if embeddedenvd.HasDefaultBinary() {
data := embeddedenvd.DefaultBinary()
if err := validateEnvdUploadBytes("embedded envd", data); err != nil {
return nil, err
}
return &envdUploadPayload{Data: data, Source: "embedded"}, nil
}
return nil, errors.New("envd injection is enabled, but no --envd-path was provided and this cubemastercli was built without an embedded envd binary")
}

func readLocalEnvdBinary(path string) ([]byte, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("read local envd binary %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("local envd binary %q must be a regular file", path)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read local envd binary %q: %w", path, err)
}
if err := validateEnvdUploadBytes(path, data); err != nil {
return nil, err
}
return data, nil
}

func validateEnvdUploadBytes(source string, data []byte) error {
if len(data) == 0 {
return fmt.Errorf("envd binary %q must not be empty", source)
}
if len(data) > maxEnvdUploadBytes {
return fmt.Errorf("envd binary %q exceeds 16MiB", source)
}
if len(data) < 4 || data[0] != 0x7f || data[1] != 'E' || data[2] != 'L' || data[3] != 'F' {
return fmt.Errorf("envd binary %q must be an ELF binary", source)
}
return nil
}

func buildCreateFromImageMultipartBody(req interface{}, payload *envdUploadPayload) (*bytes.Buffer, string, error) {
if payload == nil {
return nil, "", errors.New("envd upload payload is required")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
requestPart, err := writer.CreateFormField("request")
if err != nil {
return nil, "", err
}
encoded, err := jsoniter.Marshal(req)
if err != nil {
return nil, "", err
}
if _, err := requestPart.Write(encoded); err != nil {
return nil, "", err
}
envdPart, err := writer.CreateFormFile("envd", "envd")
if err != nil {
return nil, "", err
}
if _, err := envdPart.Write(payload.Data); err != nil {
return nil, "", err
}
if err := writer.Close(); err != nil {
return nil, "", err
}
return body, writer.FormDataContentType(), nil
}
130 changes: 130 additions & 0 deletions CubeMaster/cmd/cubemastercli/commands/cubebox/envd_upload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2026 Tencent Inc.
// SPDX-License-Identifier: Apache-2.0
//

package cubebox

import (
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/service/sandbox/types"
)

func TestSelectEnvdUploadPayloadRejectsPathWithoutInjectFlag(t *testing.T) {
ctx := newCreateFromImageContext(t, []string{"--envd-path", "/tmp/envd"})
payload, err := selectEnvdUploadPayload(ctx)
if err == nil {
t.Fatal("expected error")
}
if payload != nil {
t.Fatalf("payload=%+v, want nil", payload)
}
if !strings.Contains(err.Error(), "--envd-path requires --enable-inject-envd") {
t.Fatalf("error=%q", err.Error())
}
}

func TestSelectEnvdUploadPayloadUsesLocalEnvdPath(t *testing.T) {
path := writeCLIEnvdFixture(t, []byte{0x7f, 'E', 'L', 'F', 'o', 'k'})
ctx := newCreateFromImageContext(t, []string{"--enable-inject-envd", "--envd-path", path})
payload, err := selectEnvdUploadPayload(ctx)
if err != nil {
t.Fatalf("selectEnvdUploadPayload error=%v", err)
}
if payload == nil {
t.Fatal("expected payload")
}
if string(payload.Data) != string([]byte{0x7f, 'E', 'L', 'F', 'o', 'k'}) {
t.Fatalf("payload data=%v", payload.Data)
}
if payload.Source != path {
t.Fatalf("source=%q, want %q", payload.Source, path)
}
}

func TestBuildCreateFromImageMultipartBodyIncludesRequestAndEnvd(t *testing.T) {
payload := &envdUploadPayload{Data: []byte{0x7f, 'E', 'L', 'F', 'x'}, Source: "test"}
req := map[string]string{"source_image_ref": "ubuntu:22.04"}
body, contentType, err := buildCreateFromImageMultipartBody(req, payload)
if err != nil {
t.Fatalf("buildCreateFromImageMultipartBody error=%v", err)
}
if !strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
t.Fatalf("contentType=%q", contentType)
}
reader := multipart.NewReader(body, strings.TrimPrefix(contentType, "multipart/form-data; boundary="))
form, err := reader.ReadForm(16 * 1024 * 1024)
if err != nil {
t.Fatalf("ReadForm error=%v", err)
}
if got := form.Value["request"]; len(got) != 1 || !strings.Contains(got[0], "ubuntu:22.04") {
t.Fatalf("request field=%v", got)
}
files := form.File["envd"]
if len(files) != 1 {
t.Fatalf("envd files=%d, want 1", len(files))
}
f, err := files[0].Open()
if err != nil {
t.Fatalf("open envd part: %v", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
t.Fatalf("read envd part: %v", err)
}
if string(data) != string(payload.Data) {
t.Fatalf("envd data=%v", data)
}
}

func TestSelectEnvdUploadPayloadRejectsInvalidLocalEnvd(t *testing.T) {
path := writeCLIEnvdFixture(t, []byte("not-elf"))
ctx := newCreateFromImageContext(t, []string{"--enable-inject-envd", "--envd-path", path})
payload, err := selectEnvdUploadPayload(ctx)
if err == nil {
t.Fatal("expected error")
}
if payload != nil {
t.Fatalf("payload=%+v, want nil", payload)
}
if !strings.Contains(err.Error(), "ELF") {
t.Fatalf("error=%q", err.Error())
}
}

func writeCLIEnvdFixture(t *testing.T, data []byte) string {
t.Helper()
path := t.TempDir() + "/envd"
if err := os.WriteFile(path, data, 0o755); err != nil {
t.Fatalf("write fixture: %v", err)
}
return path
}

func TestDoHttpReqWithContentTypeUsesProvidedContentType(t *testing.T) {
var gotContentType string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotContentType = r.Header.Get("Content-Type")
_, _ = w.Write([]byte(`{"ret":{"retCode":200,"retMsg":"success"}}`))
}))
defer server.Close()

ctx := newCreateFromImageContext(t, nil)
var rsp struct {
Ret *types.Ret `json:"ret"`
}
err := doHttpReqWithContentType(ctx, server.URL, http.MethodPost, "req-1", strings.NewReader("body"), "multipart/form-data; boundary=test", &rsp)
if err != nil {
t.Fatalf("doHttpReqWithContentType error=%v", err)
}
if gotContentType != "multipart/form-data; boundary=test" {
t.Fatalf("Content-Type=%q", gotContentType)
}
}
31 changes: 27 additions & 4 deletions CubeMaster/cmd/cubemastercli/commands/cubebox/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,8 @@ var TemplateCreateFromImageCommand = cli.Command{
cli.IntFlag{Name: "cpu", Value: 2000, Usage: "CPU millicores for the template container (default: 2000, i.e. 2 cores)"},
cli.IntFlag{Name: "memory", Value: 2000, Usage: "Memory for the template container in MB (default: 2000 MB)"},
cli.BoolTFlag{Name: "with-cube-ca", Usage: "bake the CubeEgress root CA at /etc/cube/ca/cube-root-ca.crt into the template rootfs so sandboxes trust CubeEgress's MITM. Pass --with-cube-ca=false to skip (default: true)"},
cli.BoolFlag{Name: "enable-inject-envd", Usage: "enable cubesandbox-envd injection for this template build"},
cli.StringFlag{Name: "envd-path", Usage: "local envd binary path to upload when --enable-inject-envd is set; defaults to the CLI-embedded envd if available"},
cli.BoolFlag{Name: "detach, no-wait", Usage: "submit and exit immediately instead of watching the job to completion"},
cli.DurationFlag{Name: "interval", Value: defaultWatchInterval, Usage: "poll interval while watching the job"},
cli.BoolFlag{Name: "json", Usage: "print raw json response"},
Expand Down Expand Up @@ -845,14 +847,28 @@ var TemplateCreateFromImageCommand = cli.Command{
if err != nil {
return err
}
body, err := jsoniter.Marshal(req)
envdPayload, err := selectEnvdUploadPayload(c)
if err != nil {
return err
}
url := fmt.Sprintf("http://%s/cube/template/from-image", net.JoinHostPort(host, port))
rsp := &templateImageJobResponse{}
if err := doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
return err
if envdPayload != nil {
body, contentType, err := buildCreateFromImageMultipartBody(req, envdPayload)
if err != nil {
return err
}
if err := doHttpReqWithContentType(c, url, http.MethodPost, req.RequestID, body, contentType, rsp); err != nil {
return err
}
} else {
body, err := jsoniter.Marshal(req)
if err != nil {
return err
}
if err := doHttpReq(c, url, http.MethodPost, req.RequestID, bytes.NewBuffer(body), rsp); err != nil {
return err
}
}
if rsp.Ret == nil {
return errors.New("empty response")
Expand Down Expand Up @@ -1386,8 +1402,9 @@ func parseContainerOverrides(c *cli.Context) (*types.ContainerOverrides, error)
probePort := c.Int("probe")
cpuMillicores := c.Int("cpu")
memoryMB := c.Int("memory")
enableInjectEnvd := c.Bool("enable-inject-envd")

if len(cmds) == 0 && len(args) == 0 && len(rawEnvs) == 0 && len(dnsServers) == 0 && probePort == 0 && !c.IsSet("cpu") && !c.IsSet("memory") {
if len(cmds) == 0 && len(args) == 0 && len(rawEnvs) == 0 && len(dnsServers) == 0 && probePort == 0 && !c.IsSet("cpu") && !c.IsSet("memory") && !enableInjectEnvd {
return nil, nil
}

Expand Down Expand Up @@ -1442,5 +1459,11 @@ func parseContainerOverrides(c *cli.Context) (*types.ContainerOverrides, error)
SuccessThreshold: 1,
}
}
if enableInjectEnvd {
if overrides.Annotations == nil {
overrides.Annotations = map[string]string{}
}
overrides.Annotations[constants.CubeAnnotationsInjectEnvd] = constants.CubeAnnotationsInjectEnvdOptIn
}
return overrides, nil
}
15 changes: 15 additions & 0 deletions CubeMaster/cmd/cubemastercli/commands/cubebox/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"testing"

"github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/base/constants"
"github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/service/sandbox/types"
"github.com/urfave/cli"
)
Expand Down Expand Up @@ -292,6 +293,20 @@ func TestParseContainerOverridesNoDNS(t *testing.T) {
}
}

func TestParseContainerOverridesEnableInjectEnvd(t *testing.T) {
ctx := newCreateFromImageContext(t, []string{"--enable-inject-envd"})
overrides, err := parseContainerOverrides(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if overrides == nil || overrides.Annotations == nil {
t.Fatal("expected annotations to be set")
}
if got := overrides.Annotations[constants.CubeAnnotationsInjectEnvd]; got != constants.CubeAnnotationsInjectEnvdOptIn {
t.Fatalf("expected inject envd annotation=true, got %q", got)
}
}

func TestTemplateImageJobWatchPhaseLabel(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading